create-ailk 0.1.0 → 0.2.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 (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -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 +12 -5
  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 +78 -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 +241 -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 +558 -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 +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. 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
+ }