create-ailk 0.1.0 → 0.1.1

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 (605) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +295 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +22 -14
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +75 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +231 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +547 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +742 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +364 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  118. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  119. package/templates/apps/api/src/routes/project-listings/index.ts +105 -0
  120. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  121. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  122. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  123. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  124. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  125. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  126. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  127. package/templates/apps/api/src/routes/project-listings/start.ts +98 -0
  128. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  129. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  130. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  131. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  132. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  133. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  134. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  135. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  136. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  137. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  138. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  139. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  140. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  141. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  143. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  144. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  145. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  146. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  147. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  148. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  149. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  150. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  151. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  152. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  153. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  154. package/templates/apps/api/src/server.ts +293 -7
  155. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  156. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  157. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  158. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  159. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  160. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  161. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  162. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  163. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  164. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  165. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  166. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  167. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  168. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  169. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  170. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  171. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  172. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  173. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  174. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  175. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  176. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  177. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  178. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  179. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  180. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  181. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  182. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  183. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  184. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  185. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  186. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  187. package/templates/apps/api/src/services/entitlements.ts +102 -0
  188. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  189. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  190. package/templates/apps/api/src/services/flow-engine.ts +1278 -0
  191. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  192. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  193. package/templates/apps/api/src/services/listing-config.ts +102 -0
  194. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  195. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  196. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  197. package/templates/apps/api/src/services/project-listing-decision.ts +490 -0
  198. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  199. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  200. package/templates/apps/api/src/services/project-listings.ts +1124 -0
  201. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  202. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  203. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  204. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  205. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  206. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  207. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  208. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  209. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  210. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  211. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  212. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  213. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  214. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  215. package/templates/apps/api/src/vercel-handler.ts +165 -0
  216. package/templates/apps/api/tsconfig.json +0 -12
  217. package/templates/apps/mcp/CLAUDE.md +19 -9
  218. package/templates/apps/mcp/LICENSE +201 -0
  219. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  220. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  221. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  222. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  223. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  224. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  225. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  226. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  227. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  228. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  229. package/templates/apps/mcp/package.json +6 -4
  230. package/templates/apps/mcp/src/config/modules.ts +37 -0
  231. package/templates/apps/mcp/src/server.ts +109 -18
  232. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  233. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  234. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  235. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  236. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  237. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  238. package/templates/apps/mcp/src/tools/index.ts +25 -0
  239. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  240. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  241. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  242. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  243. package/templates/apps/mcp/tsconfig.json +1 -9
  244. package/templates/apps/web/.env.example +44 -0
  245. package/templates/apps/web/CLAUDE.md +28 -0
  246. package/templates/apps/web/LICENSE +201 -0
  247. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  248. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  249. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  250. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  251. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  252. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  253. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  254. package/templates/apps/web/__tests__/setup.ts +22 -32
  255. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  256. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  257. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  258. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  259. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  260. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  261. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  265. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  266. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  267. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  268. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  269. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  270. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  271. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  279. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  280. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  281. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  282. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  283. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  284. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  285. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  286. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  287. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  288. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  289. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  290. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  291. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  292. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  293. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  294. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  295. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  296. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  297. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  298. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  299. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  300. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  301. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  302. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  303. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  304. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  305. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  306. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  307. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  308. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  309. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  310. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  311. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  312. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  313. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  314. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  315. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  316. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  317. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  318. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  323. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  324. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  325. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  326. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  327. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  328. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  329. package/templates/apps/web/app/[locale]/layout.tsx +115 -16
  330. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  331. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  332. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  333. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  334. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  335. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  336. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  337. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  339. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  340. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  341. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  342. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  343. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  344. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  345. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  346. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  347. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  348. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  349. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  350. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  351. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  352. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  353. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  354. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  355. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  356. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  357. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  358. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  359. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  360. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  361. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  362. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  363. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  364. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  365. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  366. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  367. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/globals.css +35 -0
  370. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  371. package/templates/apps/web/app/og/route.tsx +10 -2
  372. package/templates/apps/web/auth.ts +8 -0
  373. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  374. package/templates/apps/web/content/en/blog/README.txt +25 -0
  375. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  376. package/templates/apps/web/jest.config.cjs +84 -27
  377. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  378. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  379. package/templates/apps/web/lib/account-config.ts +13 -0
  380. package/templates/apps/web/lib/server-api.ts +85 -0
  381. package/templates/apps/web/lib/site-brand.tsx +118 -0
  382. package/templates/apps/web/messages/en.json +275 -7
  383. package/templates/apps/web/middleware.ts +11 -3
  384. package/templates/apps/web/next.config.mjs +8 -2
  385. package/templates/apps/web/package.json +16 -7
  386. package/templates/apps/web/postcss.config.mjs +23 -0
  387. package/templates/apps/web/public/.well-known/security.txt +3 -3
  388. package/templates/apps/web/src/lib/routes.ts +15 -2
  389. package/templates/apps/web/tsconfig.json +1 -16
  390. package/templates/content/_site.mdx +24 -25
  391. package/templates/content/en/AboutPage/about.mdx +24 -14
  392. package/templates/content/en/ContactPage/contact.mdx +23 -14
  393. package/templates/content/en/FAQPage/faq.mdx +10 -392
  394. package/templates/content/en/HomePage/home.mdx +14 -285
  395. package/templates/database/CHANGELOG.md +382 -0
  396. package/templates/database/LICENSE +201 -0
  397. package/templates/database/README.md +3 -1
  398. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  399. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  400. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  401. package/templates/database/__tests__/seed.test.ts +88 -0
  402. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  403. package/templates/database/auth/schema.prisma +521 -0
  404. package/templates/database/config/schema.prisma +234 -0
  405. package/templates/database/content/schema.prisma +263 -0
  406. package/templates/database/flows/listing-diagnostic.ts +182 -0
  407. package/templates/database/flows/presets.ts +315 -0
  408. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  409. package/templates/database/inbox/schema.prisma +476 -3
  410. package/templates/database/jest.config.cjs +8 -0
  411. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  412. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  413. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  414. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  415. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  416. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  417. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  418. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  419. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  420. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  421. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  422. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  423. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  424. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  425. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  426. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  427. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  428. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  429. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  430. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  431. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  432. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  433. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  434. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  435. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  436. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  437. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  438. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  439. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  440. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  441. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  442. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  443. package/templates/database/ops/schema.prisma +27 -0
  444. package/templates/database/package.json +42 -4
  445. package/templates/database/prisma.config.ts +1 -1
  446. package/templates/database/schema.prisma +19 -435
  447. package/templates/database/scripts/db-generate-locked.sh +0 -0
  448. package/templates/database/seed-test.ts +14 -4
  449. package/templates/database/seed.ts +148 -0
  450. package/templates/database/tenancy.ts +45 -0
  451. package/templates/database/tsconfig.flows.json +15 -0
  452. package/templates/database/tsconfig.json +7 -1
  453. package/templates/database/tsconfig.tenancy.json +14 -0
  454. package/templates/package.json +30 -55
  455. package/templates/pnpm-workspace.yaml +0 -3
  456. package/templates/project.yaml +1 -1
  457. package/templates/tsconfig.json +1 -37
  458. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  459. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  460. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  461. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  462. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  463. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  464. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  465. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  466. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  467. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  468. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  469. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  470. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  471. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  472. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  473. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  474. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  475. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  476. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  477. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  478. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  479. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  480. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  481. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  482. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  483. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  484. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  485. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  486. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  487. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  488. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  489. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  490. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  491. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  492. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  493. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  494. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  495. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  496. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  497. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  498. package/templates/content/.gitkeep +0 -0
  499. package/templates/content/CLAUDE.md +0 -85
  500. package/templates/content/ar/HomePage/home.mdx +0 -123
  501. package/templates/content/ar/_site.mdx +0 -35
  502. package/templates/content/de/_site.mdx +0 -29
  503. package/templates/content/en/.gitkeep +0 -0
  504. package/templates/content/en/AboutPage/about.schema.json +0 -82
  505. package/templates/content/en/BookCall/book-call.mdx +0 -19
  506. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  507. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  508. package/templates/content/en/CareersList/careers.mdx +0 -24
  509. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  510. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  511. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  512. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  513. package/templates/content/en/ClientsList/clients.mdx +0 -38
  514. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  515. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  516. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  517. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  518. package/templates/content/en/CoursesList/courses.mdx +0 -22
  519. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  520. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  521. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  522. package/templates/content/en/EventsList/events.mdx +0 -26
  523. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  524. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  525. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  526. package/templates/content/en/HomePage/home.schema.json +0 -88
  527. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  528. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  529. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  530. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  531. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  532. package/templates/content/en/Integration/github-actions.mdx +0 -31
  533. package/templates/content/en/Integration/openai-api.mdx +0 -26
  534. package/templates/content/en/Integration/vercel.mdx +0 -32
  535. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  536. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  537. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  538. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  539. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  540. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  541. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  542. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  543. package/templates/content/en/LandingPage/developers.mdx +0 -19
  544. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  545. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  546. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  547. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  548. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  549. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  550. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  551. package/templates/content/en/LandingPage/platform.mdx +0 -369
  552. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  553. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  554. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  555. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  556. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  557. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  558. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  559. package/templates/content/en/Organization/site-owner.mdx +0 -16
  560. package/templates/content/en/Partner/prisma.mdx +0 -31
  561. package/templates/content/en/Partner/supabase.mdx +0 -30
  562. package/templates/content/en/Partner/vercel.mdx +0 -28
  563. package/templates/content/en/PartnersList/partners.mdx +0 -22
  564. package/templates/content/en/Person/founder.mdx +0 -18
  565. package/templates/content/en/Pricing/pricing.mdx +0 -538
  566. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  567. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  568. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  569. package/templates/content/en/Product/ailk.mdx +0 -19
  570. package/templates/content/en/Product/ailk.schema.json +0 -94
  571. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  572. package/templates/content/en/Product/launch-kit.mdx +0 -28
  573. package/templates/content/en/ProductsList/products.mdx +0 -26
  574. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  575. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  576. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  577. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  578. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  579. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  580. package/templates/content/en/Service/consulting.mdx +0 -22
  581. package/templates/content/en/Service/implementation.mdx +0 -30
  582. package/templates/content/en/Service/launch-service.mdx +0 -232
  583. package/templates/content/en/Service/launch.mdx +0 -228
  584. package/templates/content/en/ServicesList/services.mdx +0 -84
  585. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  586. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  587. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  588. package/templates/content/en/TeamList/team.mdx +0 -63
  589. package/templates/content/en/TeamMember/founder.mdx +0 -72
  590. package/templates/content/en/TermsPage/terms.mdx +0 -19
  591. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  592. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  593. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  594. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  595. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  596. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  597. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  598. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  599. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  600. package/templates/content/es/_site.mdx +0 -30
  601. package/templates/content/fr/_site.mdx +0 -30
  602. package/templates/content/ja/_site.mdx +0 -30
  603. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  604. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  605. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,354 @@
1
+ /**
2
+ * @file waitlist-signups.test.ts
3
+ *
4
+ * The respondent read (#4731 D2/D3/D4/D5). Mocked Prisma with a
5
+ * where-clause-honoring in-memory Lead table (the `waitlist-dashboard.test.ts`
6
+ * discipline) so the case-insensitive email match, the site scope, the
7
+ * experimentKey payload filter, and newest-wins ordering are exercised by
8
+ * what the service actually passes to the DB — not by a static return.
9
+ * Every fence is asserted: exact seven keys; unverified → NO Lead read;
10
+ * no lead → signup_not_found; foreign site → signup_not_found; every read
11
+ * scoped; no other payload key leaves the row.
12
+ */
13
+ import { readMySignup } from "../waitlist-signups";
14
+
15
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
16
+ jest.mock("../../lib/prisma.js", () => ({
17
+ prisma: {
18
+ user: { findUnique: jest.fn() },
19
+ lead: { findFirst: jest.fn() },
20
+ waitlistScore: { findFirst: jest.fn() },
21
+ flowSession: { findFirst: jest.fn() },
22
+ },
23
+ }));
24
+
25
+ import { prisma } from "../../lib/prisma.js";
26
+
27
+ // ── Fixtures ─────────────────────────────────────────────────────────────────
28
+
29
+ const SITE = "wksp_00000000-0000-7000-8000-000000000001";
30
+ const OTHER_SITE = "wksp_00000000-0000-7000-8000-0000000000bb";
31
+ const USER_ID = "user_00000000-0000-7000-8000-0000000000a1";
32
+ const EXPERIMENT = "ailk-founder-diagnostic";
33
+
34
+ const LEAD_OLD = "lead_00000000-0000-7000-8000-0000000000a1";
35
+ const LEAD_NEW = "lead_00000000-0000-7000-8000-0000000000a2";
36
+ const LEAD_FOREIGN = "lead_00000000-0000-7000-8000-0000000000b1";
37
+
38
+ type LeadRow = {
39
+ id: string;
40
+ siteId: string;
41
+ email: string;
42
+ payload: Record<string, unknown>;
43
+ createdAt: Date;
44
+ };
45
+
46
+ const LEADS: LeadRow[] = [
47
+ {
48
+ id: LEAD_OLD,
49
+ siteId: SITE,
50
+ // Stored AS TYPED (mixed case) — the D3 normalization case.
51
+ email: "Alice@Example.com",
52
+ payload: {
53
+ experimentKey: EXPERIMENT,
54
+ waitlistKey: "early_bird",
55
+ q3: "old answer",
56
+ name: "Alice",
57
+ url: "https://alice.example",
58
+ email: "Alice@Example.com",
59
+ },
60
+ createdAt: new Date("2026-09-01T00:00:00.000Z"),
61
+ },
62
+ {
63
+ id: LEAD_NEW,
64
+ siteId: SITE,
65
+ email: "alice@example.com",
66
+ payload: {
67
+ experimentKey: EXPERIMENT,
68
+ waitlistKey: "general",
69
+ q1: "solo_builder",
70
+ q3: "Our site never ships.",
71
+ name: "Alice",
72
+ url: "https://alice.example",
73
+ email: "alice@example.com",
74
+ aeoScore: { status: "unavailable", reason: "crawl_failed", message: "x" },
75
+ },
76
+ createdAt: new Date("2026-09-03T00:00:00.000Z"),
77
+ },
78
+ {
79
+ id: LEAD_FOREIGN,
80
+ siteId: OTHER_SITE,
81
+ email: "alice@example.com",
82
+ payload: { experimentKey: EXPERIMENT, waitlistKey: "foreign", q3: "foreign" },
83
+ createdAt: new Date("2026-09-04T00:00:00.000Z"),
84
+ },
85
+ ];
86
+
87
+ const SCORES = [
88
+ { leadId: LEAD_NEW, siteId: SITE, experimentKey: EXPERIMENT, tier: "hot", score: 12 },
89
+ { leadId: LEAD_OLD, siteId: SITE, experimentKey: EXPERIMENT, tier: "warm", score: 7 },
90
+ { leadId: LEAD_FOREIGN, siteId: OTHER_SITE, experimentKey: EXPERIMENT, tier: "hot", score: 99 },
91
+ ];
92
+
93
+ const AEO_UNAVAILABLE = { status: "unavailable", reason: "crawl_failed", message: "This URL cannot be scored." };
94
+
95
+ const SESSIONS = [
96
+ {
97
+ leadId: LEAD_NEW,
98
+ siteId: SITE,
99
+ aeoScore: AEO_UNAVAILABLE,
100
+ completedAt: new Date("2026-09-03T00:00:05.000Z"),
101
+ },
102
+ { leadId: LEAD_OLD, siteId: SITE, aeoScore: null, completedAt: new Date("2026-09-01T00:00:05.000Z") },
103
+ ];
104
+
105
+ // ── A where-clause-honoring in-memory matcher ────────────────────────────────
106
+
107
+ type Where = Record<string, unknown>;
108
+
109
+ function matchesLead(row: LeadRow, where: Where): boolean {
110
+ for (const [key, cond] of Object.entries(where)) {
111
+ if (key === "email") {
112
+ const c = cond as { equals: string; mode?: string };
113
+ const stored = c.mode === "insensitive" ? row.email.toLowerCase() : row.email;
114
+ const wanted = c.mode === "insensitive" ? c.equals.toLowerCase() : c.equals;
115
+ if (stored !== wanted) return false;
116
+ continue;
117
+ }
118
+ if (key === "payload") {
119
+ const { path, equals } = cond as { path: string[]; equals: unknown };
120
+ if (row.payload[path[0] as string] !== equals) return false;
121
+ continue;
122
+ }
123
+ if ((row as Record<string, unknown>)[key] !== cond) return false;
124
+ }
125
+ return true;
126
+ }
127
+
128
+ function matchesScalar(row: Record<string, unknown>, where: Where): boolean {
129
+ for (const [key, cond] of Object.entries(where)) {
130
+ if (cond !== null && typeof cond === "object" && "not" in (cond as object)) {
131
+ if (row[key] === (cond as { not: unknown }).not) return false;
132
+ continue;
133
+ }
134
+ if (row[key] !== cond) return false;
135
+ }
136
+ return true;
137
+ }
138
+
139
+ function installFixtureMocks(): void {
140
+ (prisma.lead.findFirst as jest.Mock).mockImplementation(
141
+ ({ where, orderBy }: { where: Where; orderBy?: { createdAt: "asc" | "desc" } }) => {
142
+ const rows = LEADS.filter((r) => matchesLead(r, where));
143
+ if (orderBy?.createdAt === "desc") rows.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
144
+ else rows.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
145
+ const hit = rows[0];
146
+ return Promise.resolve(hit ? { id: hit.id, payload: hit.payload, createdAt: hit.createdAt } : null);
147
+ },
148
+ );
149
+ (prisma.waitlistScore.findFirst as jest.Mock).mockImplementation(({ where }: { where: Where }) => {
150
+ const hit = SCORES.find((r) => matchesScalar(r, where));
151
+ return Promise.resolve(hit ? { tier: hit.tier, score: hit.score } : null);
152
+ });
153
+ (prisma.flowSession.findFirst as jest.Mock).mockImplementation(({ where }: { where: Where }) => {
154
+ const hit = SESSIONS.find((r) => matchesScalar(r, where));
155
+ return Promise.resolve(hit ? { aeoScore: hit.aeoScore, completedAt: hit.completedAt } : null);
156
+ });
157
+ }
158
+
159
+ function user(overrides?: Partial<{ email: string; emailVerified: boolean }>) {
160
+ return {
161
+ id: USER_ID,
162
+ email: overrides?.email ?? "alice@example.com",
163
+ emailVerified: overrides?.emailVerified ?? true,
164
+ };
165
+ }
166
+
167
+ beforeEach(() => {
168
+ jest.clearAllMocks();
169
+ process.env.AILK_SITE_ID = SITE;
170
+ installFixtureMocks();
171
+ });
172
+
173
+ afterAll(() => {
174
+ delete process.env.AILK_SITE_ID;
175
+ });
176
+
177
+ // ── Tests ────────────────────────────────────────────────────────────────────
178
+
179
+ describe("readMySignup — the verified, found path (D5)", () => {
180
+ it("returns EXACTLY the seven contract keys and only q3 under answers", async () => {
181
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
182
+
183
+ const result = await readMySignup(USER_ID, EXPERIMENT);
184
+
185
+ expect(result.ok).toBe(true);
186
+ if (!result.ok) throw new Error("unreachable");
187
+ expect(Object.keys(result.value).sort()).toEqual(
188
+ ["answers", "aeoScore", "completedAt", "experimentKey", "score", "tier", "waitlistKey"].sort(),
189
+ );
190
+ expect(Object.keys(result.value.answers)).toEqual(["q3"]);
191
+ expect(result.value).toEqual({
192
+ experimentKey: EXPERIMENT,
193
+ waitlistKey: "general",
194
+ completedAt: "2026-09-03T00:00:05.000Z",
195
+ tier: "hot",
196
+ score: 12,
197
+ aeoScore: AEO_UNAVAILABLE,
198
+ answers: { q3: "Our site never ships." },
199
+ });
200
+ // Nothing else from the payload leaves the row.
201
+ const serialized = JSON.stringify(result.value);
202
+ for (const leaked of ["name", "url", "q1", "solo_builder", "alice.example", "@example.com"]) {
203
+ expect(serialized).not.toContain(leaked);
204
+ }
205
+ });
206
+
207
+ it("reads the User exactly as lead-promotion.ts does (id, email, emailVerified by id)", async () => {
208
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
209
+
210
+ await readMySignup(USER_ID, EXPERIMENT);
211
+
212
+ expect(prisma.user.findUnique).toHaveBeenCalledWith({
213
+ where: { id: USER_ID },
214
+ select: { id: true, email: true, emailVerified: true },
215
+ });
216
+ });
217
+
218
+ it("newest createdAt wins when two leads match (AC-5)", async () => {
219
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
220
+
221
+ const result = await readMySignup(USER_ID, EXPERIMENT);
222
+
223
+ expect(result.ok && result.value.waitlistKey).toBe("general"); // LEAD_NEW, not LEAD_OLD
224
+ expect(prisma.lead.findFirst).toHaveBeenCalledWith(
225
+ expect.objectContaining({ orderBy: { createdAt: "desc" } }),
226
+ );
227
+ });
228
+
229
+ it("matches a mixed-case stored Lead.email case-insensitively on the trimmed user email (D3)", async () => {
230
+ // Only the OLD lead carries the mixed-case stored email; pick it by
231
+ // restricting the fixture to that row.
232
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user({ email: " ALICE@example.COM " }));
233
+ (prisma.lead.findFirst as jest.Mock).mockImplementation(({ where }: { where: Where }) => {
234
+ const hit = LEADS.filter((r) => r.id === LEAD_OLD).find((r) => matchesLead(r, where));
235
+ return Promise.resolve(hit ? { id: hit.id, payload: hit.payload, createdAt: hit.createdAt } : null);
236
+ });
237
+
238
+ const result = await readMySignup(USER_ID, EXPERIMENT);
239
+
240
+ expect(result.ok && result.value.waitlistKey).toBe("early_bird");
241
+ const call = (prisma.lead.findFirst as jest.Mock).mock.calls[0][0] as { where: Where };
242
+ expect(call.where.email).toEqual({ equals: "alice@example.com", mode: "insensitive" });
243
+ });
244
+
245
+ it("scopes EVERY read to currentSiteId() and filters the Lead by payload.experimentKey (AC-7)", async () => {
246
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
247
+
248
+ await readMySignup(USER_ID, EXPERIMENT);
249
+
250
+ const leadWhere = (prisma.lead.findFirst as jest.Mock).mock.calls[0][0].where as Where;
251
+ expect(leadWhere.siteId).toBe(SITE);
252
+ expect(leadWhere.payload).toEqual({ path: ["experimentKey"], equals: EXPERIMENT });
253
+ const scoreWhere = (prisma.waitlistScore.findFirst as jest.Mock).mock.calls[0][0].where as Where;
254
+ expect(scoreWhere).toEqual({ leadId: LEAD_NEW, siteId: SITE, experimentKey: EXPERIMENT });
255
+ const sessionWhere = (prisma.flowSession.findFirst as jest.Mock).mock.calls[0][0].where as Where;
256
+ expect(sessionWhere).toEqual({ leadId: LEAD_NEW, siteId: SITE, completedAt: { not: null } });
257
+ });
258
+
259
+ it("a lead that exists only at another site is signup_not_found (never cross-site)", async () => {
260
+ process.env.AILK_SITE_ID = "wksp_00000000-0000-7000-8000-0000000000cc";
261
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
262
+
263
+ const result = await readMySignup(USER_ID, EXPERIMENT);
264
+
265
+ expect(result).toEqual({
266
+ ok: false,
267
+ error: { kind: "signup_not_found", message: "Signup not found" },
268
+ });
269
+ });
270
+
271
+ it("returns null tier/score when the fail-soft score row is absent, and null aeoScore when unparsable", async () => {
272
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
273
+ (prisma.waitlistScore.findFirst as jest.Mock).mockResolvedValue(null);
274
+ (prisma.flowSession.findFirst as jest.Mock).mockResolvedValue({
275
+ aeoScore: { status: "bogus" },
276
+ completedAt: new Date("2026-09-03T00:00:05.000Z"),
277
+ });
278
+
279
+ const result = await readMySignup(USER_ID, EXPERIMENT);
280
+
281
+ expect(result.ok && result.value.tier).toBeNull();
282
+ expect(result.ok && result.value.score).toBeNull();
283
+ expect(result.ok && result.value.aeoScore).toBeNull();
284
+ });
285
+
286
+ it("falls back to Lead.createdAt for completedAt when no completing session exists, and q3 null when absent", async () => {
287
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
288
+ (prisma.flowSession.findFirst as jest.Mock).mockResolvedValue(null);
289
+ (prisma.lead.findFirst as jest.Mock).mockResolvedValue({
290
+ id: LEAD_NEW,
291
+ payload: { experimentKey: EXPERIMENT, waitlistKey: "general", q3: 42 },
292
+ createdAt: new Date("2026-09-03T00:00:00.000Z"),
293
+ });
294
+
295
+ const result = await readMySignup(USER_ID, EXPERIMENT);
296
+
297
+ expect(result.ok && result.value.completedAt).toBe("2026-09-03T00:00:00.000Z");
298
+ expect(result.ok && result.value.answers.q3).toBeNull();
299
+ });
300
+ });
301
+
302
+ describe("readMySignup — the refusals (D2/D4)", () => {
303
+ it("email_unverified: refuses BEFORE any Lead read (AC-3)", async () => {
304
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user({ emailVerified: false }));
305
+
306
+ const result = await readMySignup(USER_ID, EXPERIMENT);
307
+
308
+ expect(result.ok).toBe(false);
309
+ expect(!result.ok && result.error.kind).toBe("email_unverified");
310
+ expect(prisma.lead.findFirst).not.toHaveBeenCalled();
311
+ expect(prisma.waitlistScore.findFirst).not.toHaveBeenCalled();
312
+ expect(prisma.flowSession.findFirst).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it("not_found_user: no User row → no Lead read", async () => {
316
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
317
+
318
+ const result = await readMySignup(USER_ID, EXPERIMENT);
319
+
320
+ expect(!result.ok && result.error.kind).toBe("not_found_user");
321
+ expect(prisma.lead.findFirst).not.toHaveBeenCalled();
322
+ });
323
+
324
+ it("signup_not_found: a different email owns the leads (AC-4)", async () => {
325
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user({ email: "bob@example.com" }));
326
+
327
+ const result = await readMySignup(USER_ID, EXPERIMENT);
328
+
329
+ expect(!result.ok && result.error.kind).toBe("signup_not_found");
330
+ expect(prisma.waitlistScore.findFirst).not.toHaveBeenCalled();
331
+ });
332
+
333
+ it("signup_not_found: no lead for this experimentKey (AC-4)", async () => {
334
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
335
+
336
+ const result = await readMySignup(USER_ID, "some-other-experiment");
337
+
338
+ expect(!result.ok && result.error.kind).toBe("signup_not_found");
339
+ });
340
+
341
+ it("signup_not_found: a corrupt lead missing waitlistKey is not-found, never an out-of-contract value", async () => {
342
+ (prisma.user.findUnique as jest.Mock).mockResolvedValue(user());
343
+ (prisma.lead.findFirst as jest.Mock).mockResolvedValue({
344
+ id: LEAD_NEW,
345
+ payload: { experimentKey: EXPERIMENT, q3: "x" },
346
+ createdAt: new Date("2026-09-03T00:00:00.000Z"),
347
+ });
348
+
349
+ const result = await readMySignup(USER_ID, EXPERIMENT);
350
+
351
+ expect(!result.ok && result.error.kind).toBe("signup_not_found");
352
+ expect(prisma.waitlistScore.findFirst).not.toHaveBeenCalled();
353
+ });
354
+ });
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @file abandon-email.ts
3
+ * @description The abandon-email sweep service (#3962 D4).
4
+ *
5
+ * `runAbandonEmailSweep` is the one entry point: select partial `FlowSession`
6
+ * rows past their flow's configured delay, dispatch per the flow's send
7
+ * policy (dark → audit-only stamp; enabled → send + stamp), and record the
8
+ * outcome on the session row (D5's idempotency + audit guarantee). Invoked
9
+ * by the CLI entry (`src/bin/abandon-sweep.ts`) on an operator-supplied
10
+ * cadence — this service itself is stateless and has no timer of its own
11
+ * (D1: no queue/cron infrastructure).
12
+ *
13
+ * Mirrors the lead-routing/flow-engine house shape: type-result returns,
14
+ * mocked-dep tests, PII-safe logging via @working-theory/observability
15
+ * (siteId/flowSlug/sessionId/mode/outcome only — never email/state values).
16
+ */
17
+
18
+ import { sendAbandonEmail } from "@working-theory/email";
19
+ import { logger } from "@working-theory/observability";
20
+ import {
21
+ flowConfigSchema,
22
+ type AbandonEmailConfig,
23
+ } from "@working-theory/validation";
24
+
25
+ import { prisma } from "../lib/prisma.js";
26
+
27
+ // ─── Types ────────────────────────────────────────────────────────────────────
28
+
29
+ export type AbandonEmailSweepSummary = {
30
+ scanned: number;
31
+ selected: number;
32
+ sent: number;
33
+ dark: number;
34
+ failed: number;
35
+ skipped: number;
36
+ };
37
+
38
+ export type AbandonEmailSweepError = {
39
+ kind: "flows_not_configured" | "unexpected_error";
40
+ message: string;
41
+ };
42
+
43
+ export type AbandonEmailSweepResult =
44
+ | { ok: true; summary: AbandonEmailSweepSummary }
45
+ | { ok: false; error: AbandonEmailSweepError };
46
+
47
+ export type RunAbandonEmailSweepOptions = {
48
+ siteId?: string;
49
+ now?: Date;
50
+ };
51
+
52
+ type FlowDefinitionRow = {
53
+ id: string;
54
+ siteId: string;
55
+ slug: string;
56
+ config: unknown;
57
+ };
58
+
59
+ type AbandonCandidateRow = {
60
+ id: string;
61
+ siteId: string;
62
+ flowSlug: string;
63
+ email: string | null;
64
+ locale: string | null;
65
+ };
66
+
67
+ // ─── DB-required guard (mirrors flow-engine.ts's requireDb) ────────────────────
68
+
69
+ const FLOWS_NOT_CONFIGURED_ERROR = {
70
+ kind: "flows_not_configured" as const,
71
+ message: "Flows are not configured.",
72
+ };
73
+
74
+ function requireDb(): AbandonEmailSweepError | null {
75
+ if (!process.env.DATABASE_URL) return FLOWS_NOT_CONFIGURED_ERROR;
76
+ return null;
77
+ }
78
+
79
+ // ─── PII-safe outcome logging ───────────────────────────────────────────────────
80
+
81
+ type AbandonOutcome = "dark" | "skipped" | "sent" | "failed";
82
+
83
+ const OUTCOME_MESSAGE: Record<AbandonOutcome, string> = {
84
+ dark: "[abandon-email] session selected — dark mode, audit-only",
85
+ skipped: "[abandon-email] enabled candidate skipped — no from-address configured",
86
+ sent: "[abandon-email] session selected — sent",
87
+ failed: "[abandon-email] session selected — send failed",
88
+ };
89
+
90
+ /**
91
+ * The one shape every outcome logs (siteId/flowSlug/sessionId/mode/outcome —
92
+ * never email/state values, per this file's PII boundary).
93
+ */
94
+ function logOutcome(
95
+ session: AbandonCandidateRow,
96
+ mode: AbandonEmailConfig["mode"],
97
+ outcome: AbandonOutcome,
98
+ ): void {
99
+ logger.info(
100
+ {
101
+ siteId: session.siteId,
102
+ flowSlug: session.flowSlug,
103
+ sessionId: session.id,
104
+ mode,
105
+ outcome,
106
+ },
107
+ OUTCOME_MESSAGE[outcome],
108
+ );
109
+ }
110
+
111
+ // ─── Copy resolution (D6) ───────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * `copy[session.locale ?? liveConfig.defaultLocale] ?? copy[liveConfig.defaultLocale]`
115
+ * (D6). `defaultLocale` is a required key on every LocalizedString the
116
+ * `abandonEmailSchema` superRefine walk enforces, so the final fallback is
117
+ * always resolvable for a schema-valid config.
118
+ */
119
+ function resolveCopy(
120
+ copy: Record<string, string>,
121
+ sessionLocale: string | null,
122
+ defaultLocale: string,
123
+ ): string {
124
+ const locale = sessionLocale ?? defaultLocale;
125
+ return copy[locale] ?? copy[defaultLocale] ?? "";
126
+ }
127
+
128
+ /** Link = resumeUrl + '?fs=' + sessionId — no other session data (D7, no PII). */
129
+ function buildResumeLink(
130
+ resumeUrl: string | undefined,
131
+ sessionId: string,
132
+ ): string | undefined {
133
+ if (!resumeUrl) return undefined;
134
+ return `${resumeUrl}?fs=${sessionId}`;
135
+ }
136
+
137
+ // ─── The sweep ──────────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * The unguarded sweep body — mutates `summary` in place as it goes, so a
141
+ * mid-loop throw still leaves the caller's catch block able to log how far
142
+ * it got (code-review finding on #3962's acceptance PR: an unhandled
143
+ * rejection mid-sweep should degrade to a structured error with the partial
144
+ * progress visible, not vanish).
145
+ */
146
+ async function sweepAllDefinitions(
147
+ opts: RunAbandonEmailSweepOptions,
148
+ now: Date,
149
+ fromAddress: string | undefined,
150
+ summary: AbandonEmailSweepSummary,
151
+ ): Promise<void> {
152
+ const definitions = (await prisma.flowDefinition.findMany({
153
+ ...(opts.siteId !== undefined && { where: { siteId: opts.siteId } }),
154
+ select: { id: true, siteId: true, slug: true, config: true },
155
+ })) as FlowDefinitionRow[];
156
+
157
+ for (const definition of definitions) {
158
+ const parsedConfig = flowConfigSchema.safeParse(definition.config);
159
+ if (!parsedConfig.success) {
160
+ // Surface, do not swallow (code-review finding on #3962's acceptance
161
+ // PR) — every sibling path in this feature area (serveFlow /
162
+ // upsertFlowDefinition in flow-engine.ts) returns an explicit error on
163
+ // a config-parse failure; a stored config that has drifted from the
164
+ // schema must not silently stop that flow's abandon-email policy from
165
+ // ever running again with no operator-visible signal.
166
+ logger.warn(
167
+ {
168
+ siteId: definition.siteId,
169
+ flowSlug: definition.slug,
170
+ zodIssue: parsedConfig.error.issues[0]?.message,
171
+ },
172
+ "[abandon-email] skipping flow — stored config failed schema validation",
173
+ );
174
+ continue;
175
+ }
176
+ const config = parsedConfig.data;
177
+ const policy: AbandonEmailConfig | undefined = config.abandonEmail;
178
+ // Block absent = abandon email OFF for this flow — never selected (D2).
179
+ if (!policy) continue;
180
+
181
+ const cutoff = new Date(now.getTime() - policy.delayMinutes * 60_000);
182
+
183
+ // The #3956 D2 partial-lead query (email != null AND completedAt == null)
184
+ // plus the not-yet-processed and delay-window terms (D4/D5).
185
+ const candidates = (await prisma.flowSession.findMany({
186
+ where: {
187
+ flowId: definition.id,
188
+ email: { not: null },
189
+ completedAt: null,
190
+ abandonEmailStatus: null,
191
+ updatedAt: { lte: cutoff },
192
+ },
193
+ select: {
194
+ id: true,
195
+ siteId: true,
196
+ flowSlug: true,
197
+ email: true,
198
+ locale: true,
199
+ },
200
+ })) as AbandonCandidateRow[];
201
+
202
+ summary.scanned += candidates.length;
203
+
204
+ for (const session of candidates) {
205
+ summary.selected += 1;
206
+
207
+ if (policy.mode === "dark") {
208
+ await prisma.flowSession.update({
209
+ where: { id: session.id },
210
+ data: { abandonEmailStatus: "dark", abandonEmailAt: now },
211
+ });
212
+ summary.dark += 1;
213
+ logOutcome(session, policy.mode, "dark");
214
+ continue;
215
+ }
216
+
217
+ // mode:'enabled' — an env gap must not consume the one send: leave the
218
+ // candidate UNSTAMPED so a later configured run still sends it (D4).
219
+ if (!fromAddress) {
220
+ summary.skipped += 1;
221
+ logOutcome(session, policy.mode, "skipped");
222
+ continue;
223
+ }
224
+
225
+ const subject = resolveCopy(
226
+ policy.copy.subject,
227
+ session.locale,
228
+ config.defaultLocale,
229
+ );
230
+ const body = resolveCopy(
231
+ policy.copy.body,
232
+ session.locale,
233
+ config.defaultLocale,
234
+ );
235
+ const cta = policy.copy.cta
236
+ ? resolveCopy(policy.copy.cta, session.locale, config.defaultLocale)
237
+ : undefined;
238
+ const resumeLink = buildResumeLink(policy.resumeUrl, session.id);
239
+
240
+ const sendResult = await sendAbandonEmail({
241
+ // `email: { not: null }` in the WHERE clause above guarantees this.
242
+ to: session.email as string,
243
+ fromAddress,
244
+ subject,
245
+ body,
246
+ ...(cta !== undefined && { cta }),
247
+ ...(resumeLink !== undefined && { resumeLink }),
248
+ });
249
+
250
+ if (sendResult.ok) {
251
+ await prisma.flowSession.update({
252
+ where: { id: session.id },
253
+ data: { abandonEmailStatus: "sent", abandonEmailAt: now },
254
+ });
255
+ summary.sent += 1;
256
+ logOutcome(session, policy.mode, "sent");
257
+ } else {
258
+ // Terminal to the sweep — no auto-retry (D5); clearing the stamp is
259
+ // the deliberate operator retry.
260
+ await prisma.flowSession.update({
261
+ where: { id: session.id },
262
+ data: { abandonEmailStatus: "failed", abandonEmailAt: now },
263
+ });
264
+ summary.failed += 1;
265
+ logOutcome(session, policy.mode, "failed");
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ export async function runAbandonEmailSweep(
272
+ opts: RunAbandonEmailSweepOptions = {},
273
+ ): Promise<AbandonEmailSweepResult> {
274
+ const dbError = requireDb();
275
+ if (dbError) return { ok: false, error: dbError };
276
+
277
+ const now = opts.now ?? new Date();
278
+ const fromAddress =
279
+ process.env.AILK_ABANDON_EMAIL_FROM ??
280
+ process.env.AILK_LEAD_NOTIFICATION_FROM;
281
+
282
+ const summary: AbandonEmailSweepSummary = {
283
+ scanned: 0,
284
+ selected: 0,
285
+ sent: 0,
286
+ dark: 0,
287
+ failed: 0,
288
+ skipped: 0,
289
+ };
290
+
291
+ try {
292
+ await sweepAllDefinitions(opts, now, fromAddress, summary);
293
+ } catch (err) {
294
+ // An unexpected DB/network failure mid-loop must degrade to a
295
+ // structured error, not an unhandled rejection (code-review finding on
296
+ // #3962's acceptance PR) — the CLI entry's own try/catch expects this
297
+ // shape. Progress already stamped on session rows before the throw is
298
+ // NOT lost (D5's idempotency means a re-run picks up where this one
299
+ // stopped); the partial in-memory summary is logged so it is not
300
+ // silently discarded.
301
+ const message = err instanceof Error ? err.message : "Unknown error";
302
+ logger.error({ partialSummary: summary, err: message }, "[abandon-email] sweep failed mid-run");
303
+ return { ok: false, error: { kind: "unexpected_error", message } };
304
+ }
305
+
306
+ return { ok: true, summary };
307
+ }