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,510 +0,0 @@
1
- /**
2
- * @file project-listing-decision.ts
3
- * @description The one Approve/Reject transition for a `ProjectListing`
4
- * (#4999) — the SAME function `PATCH /v1/project-listings/:id` (tenant-scoped,
5
- * via `routes/project-listings/patch.ts`) and `POST /v1/slack/actions` (the
6
- * Slack button click, no tenant context) both call, so the stamp + backlink
7
- * logic exists in exactly one place.
8
- *
9
- * `siteId` is optional: the tenant-scoped PATCH route passes it (so a foreign
10
- * listing id 404s, matching every other tenant-scoped read/write in this
11
- * service group); the Slack handler omits it (the button's `value` carries
12
- * only the listing id — no tenant context to check).
13
- *
14
- * Approve stamps `windowEndsAt = decidedAt + the row's windowDays`, the
15
- * applicant's requested window already clamped at create against the site's
16
- * `SiteListingConfig` (#5033 D4). The ninety days this used to hardcode were
17
- * one site's commercial terms; there is no duration constant here now. A row
18
- * already decided (`status` already `active` or `rejected`) is idempotent —
19
- * `{ ok: true, changed: false }`, the row untouched, no second stamp — the
20
- * "second click" AC (#4999): both the PATCH route (an operator re-submitting
21
- * the same decision) and the Slack route (a stale button click after the
22
- * message has already updated) see the same 200/no-op shape. Only a listing
23
- * still `requested` can transition; anything else (an already-decided row
24
- * asking for a DIFFERENT decision, or a `draft` asking to be decided) is
25
- * `invalid_transition`.
26
- *
27
- * `value` is a strict `ProjectListing` (safe to pass straight through
28
- * `routes/project-listings/respond.ts#sendValidated`, which re-parses
29
- * against `projectListingSchema.strict()`); `slackChannel`/`slackTs` live
30
- * OUTSIDE it, in the sibling `slack` field, so an unexpected key never
31
- * reaches that contract check. The row shape/select and the `waitlistCount`
32
- * join still mirror `services/project-listings.ts` rather than importing it,
33
- * but the one piece that MUST NOT drift — the computed `closed` state — is
34
- * now imported from there (`computeListingStatus`), so both modules resolve a
35
- * row's lifecycle status through the same function.
36
- *
37
- * Also exports `sendListingDecisionEmail` — the founder email dispatch
38
- * `routes/slack/actions.ts` (on a changed Slack decision) and
39
- * `routes/project-listings/patch.ts` (on a changed PATCH decision) both
40
- * call, so the founder is emailed exactly once regardless of which channel
41
- * decided. See that function's own doc comment for the env-var reuse this
42
- * issue makes (no new email-specific env var beyond the three Slack ones).
43
- * On an APPROVE, it also fires `fileListingIssue` (#5000,
44
- * `project-listing-issue.ts`) fire-and-forget after the email — the one
45
- * work-order GitHub issue for the row, on the site's own repo. Both
46
- * callers reach it through this one function, so the filing (like the
47
- * email) fires exactly once per changed decision, never per caller.
48
- *
49
- * Also exports `onListingDecided` — the approve-path side-effect seam (#5001):
50
- * a `requested` → `active` transition projects the row into its live
51
- * diagnostic (`generateListingFlow`). `decideListing` calls it after a CHANGED
52
- * approve, and since #5033 collapsed the lifecycle that is the ONLY caller —
53
- * `requested → active | rejected` is the only transition there is, and both
54
- * the PATCH route and the Slack click reach it through `decideListing`.
55
- * Fail-soft by construction — a projection failure is logged
56
- * and swallowed; the decision has already committed. #5000 (the build issue)
57
- * joins the same seam.
58
- *
59
- * PII boundary: `decideListing` never logs (the DB mutation only).
60
- * `sendListingDecisionEmail` logs `listingId`/`decision`/an error KIND only —
61
- * never founderEmail/founderName/questions (mirrors
62
- * `services/project-listings.ts` and `services/lead-routing.ts`'s
63
- * discipline).
64
- */
65
-
66
- import type { Prisma } from "@working-theory/database";
67
- import { sendListingApprovedEmail, sendListingRejectedEmail } from "@working-theory/email";
68
- import { logger } from "@working-theory/observability";
69
- import {
70
- clampWindowDays,
71
- projectListingQuestionsSchema,
72
- type ProjectListing,
73
- type ProjectListingFaqs,
74
- type ProjectListingQuestions,
75
- } from "@working-theory/validation";
76
-
77
- import { config } from "../config/index.js";
78
- import { prisma } from "../lib/prisma.js";
79
-
80
- import { listingConfigFor } from "./listing-config.js";
81
- import { generateListingFlow } from "./project-listing-flow.js";
82
- import { fileListingIssue } from "./project-listing-issue.js";
83
- import {
84
- addressPartsFromRow,
85
- computeListingStatus,
86
- siteAnswersFromRow,
87
- } from "./project-listings.js";
88
-
89
- export type ListingDecision = "active" | "rejected";
90
-
91
- /** Days to milliseconds. A unit conversion, not a policy duration. */
92
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
93
-
94
- const SELECT = {
95
- id: true,
96
- siteId: true,
97
- leadId: true,
98
- slug: true,
99
- name: true,
100
- oneLiner: true,
101
- problem: true,
102
- url: true,
103
- product: true,
104
- commitment: true,
105
- teamComposition: true,
106
- faqs: true,
107
- anythingElse: true,
108
- address: true,
109
- // #5279 — the structured parts beside the display string.
110
- addressLine1: true,
111
- addressLine2: true,
112
- addressCity: true,
113
- addressRegion: true,
114
- addressPostalCode: true,
115
- addressCountry: true,
116
- questions: true,
117
- // #5285 — the operator's review payload carries EVERY site answer, the
118
- // `operator-only` ones included. That visibility is what it is for: an
119
- // answer a site asked for its own triage reaches the reviewer and stops
120
- // short of the public read, which narrows through `publicSiteAnswers`.
121
- siteAnswers: true,
122
- founderName: true,
123
- founderEmail: true,
124
- founderCompany: true,
125
- headline: true,
126
- logoMarkUrl: true,
127
- logoWordmarkUrl: true,
128
- featured: true,
129
- status: true,
130
- consentAt: true,
131
- decidedAt: true,
132
- decidedBy: true,
133
- issueUrl: true,
134
- experimentKey: true,
135
- windowDays: true,
136
- windowEndsAt: true,
137
- slackChannel: true,
138
- slackTs: true,
139
- createdAt: true,
140
- updatedAt: true,
141
- } as const satisfies Prisma.ProjectListingSelect;
142
-
143
- type Row = Prisma.ProjectListingGetPayload<{ select: typeof SELECT }>;
144
-
145
- /**
146
- * Mirrors `project-listings.ts#attachWaitlistCounts`'s join shape for the
147
- * single row this module ever handles.
148
- *
149
- * Since #5036 D6d the listing stores the experiment's KEY, so this is one
150
- * count against `WaitlistScore.experimentKey` — the `WaitlistExperiment` read
151
- * that existed only to translate an id into that key is gone from both
152
- * modules.
153
- */
154
- async function waitlistCountFor(row: Row): Promise<number> {
155
- if (!row.experimentKey) return 0;
156
- return prisma.waitlistScore.count({
157
- where: { siteId: row.siteId, experimentKey: row.experimentKey },
158
- });
159
- }
160
-
161
- function toListing(row: Row, waitlistCount: number): ProjectListing {
162
- return {
163
- id: row.id,
164
- siteId: row.siteId,
165
- leadId: row.leadId,
166
- slug: row.slug,
167
- name: row.name,
168
- headline: row.headline,
169
- oneLiner: row.oneLiner,
170
- problem: row.problem,
171
- url: row.url,
172
- product: row.product as ProjectListing["product"],
173
- commitment: row.commitment as ProjectListing["commitment"],
174
- teamComposition: row.teamComposition as ProjectListing["teamComposition"],
175
- faqs: row.faqs as unknown as ProjectListingFaqs | null,
176
- anythingElse: row.anythingElse,
177
- address: row.address,
178
- // #5279 — one reader of the parts, shared with `project-listings.ts`, so
179
- // the two row renderers cannot drift on what "has parts" means.
180
- addressParts: addressPartsFromRow(row),
181
- founderName: row.founderName,
182
- founderEmail: row.founderEmail,
183
- founderCompany: row.founderCompany,
184
- questions: row.questions as unknown as ProjectListingQuestions | null,
185
- siteAnswers: siteAnswersFromRow(row.siteAnswers),
186
- logoMarkUrl: row.logoMarkUrl,
187
- logoWordmarkUrl: row.logoWordmarkUrl,
188
- featured: row.featured,
189
- status: computeListingStatus(row.status, row.windowEndsAt, new Date(), row.featured),
190
- consentAt: row.consentAt.toISOString(),
191
- decidedAt: row.decidedAt ? row.decidedAt.toISOString() : null,
192
- decidedBy: row.decidedBy,
193
- issueUrl: row.issueUrl,
194
- experimentKey: row.experimentKey,
195
- windowEndsAt: row.windowEndsAt ? row.windowEndsAt.toISOString() : null,
196
- waitlistCount,
197
- createdAt: row.createdAt.toISOString(),
198
- updatedAt: row.updatedAt.toISOString(),
199
- };
200
- }
201
-
202
- export type DecideListingInput = {
203
- id: string;
204
- decision: ListingDecision;
205
- decidedBy: string;
206
- /** Tenant scope for the PATCH route; omitted by the Slack handler (no tenant context on a button click). */
207
- siteId?: string;
208
- };
209
-
210
- export type DecideListingResult =
211
- | {
212
- ok: true;
213
- changed: boolean;
214
- value: ProjectListing;
215
- /** The row's posted-message coordinates, for the Slack route's `chat.update` — never part of `value`. */
216
- slack: { channel: string | null; ts: string | null };
217
- }
218
- | {
219
- ok: false;
220
- error: { kind: "not_found" | "invalid_transition"; message: string };
221
- };
222
-
223
- const DECIDED_STATUSES: ReadonlySet<string> = new Set(["active", "rejected"]);
224
-
225
- /**
226
- * The window, in whole days, to stamp on an approve. The row's own
227
- * `windowDays` (the applicant's request, clamped at create against the site's
228
- * terms) when it has one; otherwise the site's configured default clamped the
229
- * same way, for a row that was already pending when the column shipped.
230
- * `null` when the row does not exist — the caller's fresh read reports that.
231
- */
232
- async function windowDaysFor(id: string, siteId: string | undefined): Promise<number | null> {
233
- const row = await prisma.projectListing.findFirst({
234
- where: siteId !== undefined ? { id, siteId } : { id },
235
- select: { siteId: true, windowDays: true },
236
- });
237
- if (!row) return null;
238
- if (row.windowDays !== null) return row.windowDays;
239
- return clampWindowDays(undefined, await listingConfigFor(row.siteId));
240
- }
241
-
242
- /**
243
- * The transition write is a GUARDED `updateMany` — `where` includes
244
- * `status: "applied"`, so the write only lands if the row is STILL `applied`
245
- * at the moment Postgres evaluates it (row-level locking makes this atomic;
246
- * mirrors the compare-and-swap shape `routes/webhooks/stripe.ts#stampFlowCheckout`
247
- * uses on `FlowCheckout.status`). `count === 1` means THIS call made the
248
- * transition; `count === 0` means it didn't — either the row was never
249
- * `applied` to begin with, or a CONCURRENT call already decided it between
250
- * this call's own read and write (security review, PR #4999: a plain
251
- * read-then-write here would let two racing calls — e.g. Slack's own retry,
252
- * or a Slack click racing an operator's PATCH — both observe `applied` and
253
- * both "succeed", double-sending the founder email and possibly leaving the
254
- * row in whichever decision wrote last). Either way, a fresh read after the
255
- * guarded write tells the caller which: already decided (idempotent
256
- * `changed: false`) or a genuinely invalid transition (`live`/`closed`).
257
- */
258
- export async function decideListing({
259
- id,
260
- decision,
261
- decidedBy,
262
- siteId,
263
- }: DecideListingInput): Promise<DecideListingResult> {
264
- const decidedAt = new Date();
265
-
266
- // #5033 — the window this listing was granted, resolved BEFORE the guarded
267
- // write because a `updateMany` cannot read a column it is also setting.
268
- // `windowDays` is immutable once stamped at create, so reading it outside
269
- // the guard is not part of the compare-and-swap. A row that predates the
270
- // column (an application still pending when this shipped) has none, and
271
- // takes the site's configured default instead.
272
- const windowDays = await windowDaysFor(id, siteId);
273
-
274
- const updateResult = await prisma.projectListing.updateMany({
275
- where: siteId !== undefined ? { id, siteId, status: "requested" } : { id, status: "requested" },
276
- data: {
277
- status: decision,
278
- decidedAt,
279
- decidedBy,
280
- ...(decision === "active" &&
281
- windowDays !== null && {
282
- windowEndsAt: new Date(decidedAt.getTime() + windowDays * MS_PER_DAY),
283
- }),
284
- },
285
- });
286
-
287
- const current = await prisma.projectListing.findFirst({
288
- where: siteId !== undefined ? { id, siteId } : { id },
289
- select: SELECT,
290
- });
291
- if (!current) {
292
- return { ok: false, error: { kind: "not_found", message: "Project listing not found" } };
293
- }
294
-
295
- if (updateResult.count === 1) {
296
- // #5001 — the approve's downstream effects (the diagnostic projection).
297
- // Never throws; a failure is logged and the decision stands. Re-read
298
- // afterwards so `value.experimentKey` reflects the projection.
299
- await onListingDecided(current.siteId, current.id, decision);
300
- const projected =
301
- (await prisma.projectListing.findFirst({ where: { id: current.id }, select: SELECT })) ?? current;
302
- const waitlistCount = await waitlistCountFor(projected);
303
- return {
304
- ok: true,
305
- changed: true,
306
- value: toListing(projected, waitlistCount),
307
- slack: { channel: projected.slackChannel, ts: projected.slackTs },
308
- };
309
- }
310
-
311
- // The guarded write matched nothing — the row wasn't `applied` when
312
- // Postgres evaluated the guard. Read its CURRENT status to tell an
313
- // already-decided idempotent no-op from a genuinely invalid transition.
314
- if (DECIDED_STATUSES.has(current.status)) {
315
- const waitlistCount = await waitlistCountFor(current);
316
- return {
317
- ok: true,
318
- changed: false,
319
- value: toListing(current, waitlistCount),
320
- slack: { channel: current.slackChannel, ts: current.slackTs },
321
- };
322
- }
323
-
324
- return {
325
- ok: false,
326
- error: {
327
- kind: "invalid_transition",
328
- message: `cannot transition from '${current.status}' to '${decision}'`,
329
- },
330
- };
331
- }
332
-
333
- /**
334
- * Sends the founder's approve/reject email for a just-decided listing
335
- * (#4999 step 5).
336
- *
337
- * Reasonable-call env-var reuse (this issue's footprint is the three Slack
338
- * vars only — no new email-specific var): `fromAddress` reads the existing
339
- * `AILK_LEAD_NOTIFICATION_FROM` (already the site's configured "From:" for
340
- * lead-facing system email, per `services/lead-routing.ts`); the Working
341
- * Session and diagnostic links are derived from the existing
342
- * `config.FRONTEND_URL` + fixed paths. The diagnostic page itself is #5001's
343
- * build — this link is a forward reference to a route that doesn't exist
344
- * yet. `brand` is left unset (the template's clean, unbranded default) since
345
- * no per-site branding source is wired into `apps/api` today.
346
- *
347
- * Never throws; a missing `AILK_LEAD_NOTIFICATION_FROM` no-ops (logs
348
- * `listing-email-skipped`, id + decision only) rather than failing the
349
- * decision that has already landed.
350
- *
351
- * On an APPROVE, also fires `fileListingIssue` (#5000) after the email —
352
- * fire-and-forget, `.catch`-guarded on top of that function's own internal
353
- * try/catch, so neither a slow GitHub call nor a filing failure can delay or
354
- * reach the approve path that has already committed. Runs regardless of the
355
- * email's own outcome (a Resend failure and a GitHub failure are independent
356
- * best-effort side effects of the same committed decision).
357
- */
358
- export async function sendListingDecisionEmail(
359
- listing: ProjectListing,
360
- decision: ListingDecision,
361
- ): Promise<void> {
362
- const fromAddress = process.env["AILK_LEAD_NOTIFICATION_FROM"];
363
- if (!fromAddress) {
364
- logger.info({ listingId: listing.id, decision }, "listing-email-skipped");
365
- fileIssueOnApprove(listing, decision);
366
- return;
367
- }
368
-
369
- const workingSession = `${config.FRONTEND_URL}/working-session`;
370
- // #5036 D1 — `name` and `slug` are nullable at rest so a half-filled draft
371
- // is storable, but a decision only ever lands on a SUBMITTED row, which
372
- // proved both. The fallbacks discharge the type, they do not paper over a
373
- // reachable state.
374
- const projectName = listing.name ?? listing.founderName;
375
- const diagnosticSlug = listing.slug ?? listing.id;
376
-
377
- const result =
378
- decision === "active"
379
- ? await sendListingApprovedEmail({
380
- to: listing.founderEmail,
381
- fromAddress,
382
- founderName: listing.founderName,
383
- projectName,
384
- links: { workingSession },
385
- })
386
- : await sendListingRejectedEmail({
387
- to: listing.founderEmail,
388
- fromAddress,
389
- founderName: listing.founderName,
390
- projectName,
391
- links: { workingSession, diagnostic: `${config.FRONTEND_URL}/diagnostic/${diagnosticSlug}` },
392
- });
393
-
394
- if (!result.ok) {
395
- logger.warn(
396
- { listingId: listing.id, decision, errorKind: result.error.kind },
397
- "listing email send failed",
398
- );
399
- }
400
-
401
- fileIssueOnApprove(listing, decision);
402
- }
403
-
404
- /**
405
- * Fire-and-forget entry to `fileListingIssue` (#5000) — a no-op on
406
- * `rejected`. `fileListingIssue` already never throws (its own try/catch
407
- * around the fetch + DB write); this `.catch` is a second boundary for the
408
- * one thing outside that try — `renderListingIssue` running before it — so
409
- * NOTHING from this call can ever propagate into `sendListingDecisionEmail`'s
410
- * caller.
411
- */
412
- function fileIssueOnApprove(listing: ProjectListing, decision: ListingDecision): void {
413
- if (decision !== "active") return;
414
- fileListingIssue(listing).catch((err: unknown) => {
415
- logger.warn(
416
- { listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
417
- "listing issue filing rejected unexpectedly",
418
- );
419
- });
420
- }
421
-
422
- // ─── #5001 — the approve-path side-effect seam ─────────────────────────────
423
-
424
- /** The status whose arrival triggers the projection — the approved state. */
425
- const APPROVED = "active";
426
-
427
- /**
428
- * Run the decision's downstream effects for one listing. Called by
429
- * `patchProjectListing` immediately after a successful transition write;
430
- * never throws.
431
- */
432
- export async function onListingDecided(
433
- siteId: string,
434
- listingId: string,
435
- toStatus: string,
436
- ): Promise<void> {
437
- if (toStatus !== APPROVED) return;
438
-
439
- try {
440
- const row = await prisma.projectListing.findFirst({
441
- where: { id: listingId, siteId },
442
- select: {
443
- id: true,
444
- siteId: true,
445
- slug: true,
446
- name: true,
447
- oneLiner: true,
448
- questions: true,
449
- },
450
- });
451
- if (!row) return;
452
-
453
- // The stored `questions` blob is validated on write (#4998's POST route)
454
- // — re-parsing here is defense in depth, and the one place a row that
455
- // predates or drifts from the template is skipped rather than projected
456
- // into a malformed flow.
457
- // #5036 D1 — `slug`/`name`/`oneLiner` are nullable at rest so a half-filled
458
- // draft is storable. A row can only reach `active` through `submit`, which
459
- // asserts all three, so this discharges a type-level possibility rather
460
- // than a reachable state — and skips rather than throwing, matching the
461
- // fail-soft contract this whole seam carries.
462
- if (row.slug === null || row.name === null || row.oneLiner === null) {
463
- logger.warn(
464
- { siteId, listingId, kind: "incomplete_listing" },
465
- "[project-listing-decision] skipping flow generation — an approved listing is missing submit-time fields",
466
- );
467
- return;
468
- }
469
-
470
- const questions = projectListingQuestionsSchema.safeParse(row.questions);
471
- if (!questions.success) {
472
- logger.warn(
473
- { siteId, listingId, slug: row.slug, kind: "invalid_questions" },
474
- "[project-listing-decision] skipping flow generation — stored questions failed schema validation",
475
- );
476
- return;
477
- }
478
-
479
- const result = await generateListingFlow({
480
- id: row.id,
481
- siteId: row.siteId,
482
- slug: row.slug,
483
- name: row.name,
484
- oneLiner: row.oneLiner,
485
- questions: questions.data,
486
- });
487
-
488
- if (!result.ok) {
489
- logger.error(
490
- { siteId, listingId, slug: row.slug, kind: result.error.kind },
491
- "[project-listing-decision] flow generation failed — the listing is approved but has no diagnostic",
492
- );
493
- return;
494
- }
495
-
496
- logger.info(
497
- { siteId, listingId, slug: row.slug, kind: "generated" },
498
- "[project-listing-decision] diagnostic flow + experiment generated",
499
- );
500
- } catch (err) {
501
- logger.error(
502
- {
503
- siteId,
504
- listingId,
505
- errKind: err instanceof Error ? err.constructor.name : typeof err,
506
- },
507
- "[project-listing-decision] approve hook failed — the transition still succeeded",
508
- );
509
- }
510
- }