create-ailk 0.2.0 → 0.3.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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -1,179 +0,0 @@
1
- /**
2
- * /[locale]/waitlist — the operator dashboard's list + rollup view
3
- * (#4665 D10, AC1).
4
- *
5
- * RSC: reads `GET /v1/waitlist-experiments` through
6
- * `@working-theory/api-client`'s `waitlistExperiments.list`, forwarding the
7
- * caller's cookie session (`../../../../lib/server-api.js`, D9). A 401/403
8
- * renders `ForbiddenState` (no leak of why — §2.2 Security); every other
9
- * error rethrows to the route's error boundary.
10
- */
11
-
12
- import { waitlistExperiments } from "@working-theory/api-client";
13
- import { PageHeader } from "@working-theory/ui/blocks";
14
- import { Container, Heading, Text } from "@working-theory/ui/primitives";
15
- import type { WaitlistMode, WaitlistStatus } from "@working-theory/validation";
16
- import type { Metadata } from "next";
17
- import { getTranslations } from "next-intl/server";
18
- import type { ReactElement } from "react";
19
-
20
- import { ApiError, forwardedCookieInit, unwrapApiResult } from "../../../../lib/server-api";
21
-
22
- import { ExperimentsTable } from "./components/ExperimentsTable";
23
- import { ForbiddenState } from "./components/ForbiddenState";
24
- import { OfferFunnelTable } from "./components/OfferFunnelTable";
25
- import { RollupTicker } from "./components/RollupTicker";
26
- import { formatCount, formatDateTime, formatRate } from "./format";
27
-
28
- interface Props {
29
- params: Promise<{ locale: string }>;
30
- }
31
-
32
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
33
- const { locale } = await params;
34
- const t = await getTranslations({ locale, namespace: "waitlistDashboard.list" });
35
- return { title: t("title"), description: t("description") };
36
- }
37
-
38
- export default async function WaitlistListPage({ params }: Props): Promise<ReactElement> {
39
- const { locale } = await params;
40
- const t = await getTranslations({ locale, namespace: "waitlistDashboard" });
41
-
42
- let data;
43
- try {
44
- data = await unwrapApiResult(waitlistExperiments.list(await forwardedCookieInit()));
45
- } catch (err) {
46
- if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
47
- return (
48
- <ForbiddenState
49
- headline={t("common.forbiddenHeadline")}
50
- description={t("common.forbiddenDescription")}
51
- />
52
- );
53
- }
54
- throw err;
55
- }
56
-
57
- const statusLabels: Record<WaitlistStatus, string> = {
58
- live: t("status.live"),
59
- closed: t("status.closed"),
60
- draft: t("status.draft"),
61
- };
62
- const modeLabels: Record<WaitlistMode, string> = {
63
- "lead-capture": t("mode.lead-capture"),
64
- qualifying: t("mode.qualifying"),
65
- paid: t("mode.paid"),
66
- // #4767 — the register's fourth mode; the engine/UI that reads it are
67
- // sibling children of Epic #4758 (out of scope here). A placeholder
68
- // label only, so `Record<WaitlistMode, …>` stays exhaustive.
69
- recommending: t("mode.recommending"),
70
- };
71
-
72
- const rows = data.experiments.map((experiment) => ({
73
- experiment,
74
- raw: formatCount(experiment.counts.raw, locale),
75
- qualified:
76
- experiment.counts.qualified === null
77
- ? t("common.unavailable")
78
- : formatCount(experiment.counts.qualified, locale),
79
- rate: formatRate(experiment.counts.qualificationRate, locale),
80
- updated: formatDateTime(experiment.updatedAt, locale),
81
- }));
82
-
83
- // #4839 — one per-offer funnel per recommending experiment (the register's
84
- // `offers[]` order), rendered under the experiments table.
85
- const offerFunnels = data.experiments.flatMap((experiment) =>
86
- experiment.counts.offers
87
- ? [
88
- {
89
- experimentId: experiment.id,
90
- experimentKey: experiment.key,
91
- rows: experiment.counts.offers.map((offer) => ({
92
- offerKey: offer.offerKey,
93
- recommended: formatCount(offer.recommended, locale),
94
- saved: formatCount(offer.saved, locale),
95
- checkedOut: formatCount(offer.checkedOut, locale),
96
- })),
97
- },
98
- ]
99
- : [],
100
- );
101
- const offerLabels = {
102
- captionPrefix: t("list.offers.captionPrefix"),
103
- columnOffer: t("list.offers.columnOffer"),
104
- columnRecommended: t("list.offers.columnRecommended"),
105
- columnSaved: t("list.offers.columnSaved"),
106
- columnCheckedOut: t("list.offers.columnCheckedOut"),
107
- };
108
-
109
- return (
110
- <main className="min-h-screen bg-surface text-fg" data-testid="waitlist-list-page">
111
- <PageHeader />
112
- <Container className="flex flex-col gap-6 py-section">
113
- <div>
114
- <Heading level={1}>{t("list.title")}</Heading>
115
- <Text as="p" variant="body-sm" tone="muted" className="mt-1">
116
- {t("list.description")}
117
- </Text>
118
- </div>
119
-
120
- <RollupTicker
121
- ariaLabel={t("list.rollupAriaLabel")}
122
- labels={{
123
- live: t("list.rollup.live"),
124
- closed: t("list.rollup.closed"),
125
- draft: t("list.rollup.draft"),
126
- raw: t("list.rollup.raw"),
127
- qualified: t("list.rollup.qualified"),
128
- rate: t("list.rollup.rate"),
129
- sites: t("list.rollup.sites"),
130
- }}
131
- values={{
132
- live: formatCount(data.rollup.experiments.live, locale),
133
- closed: formatCount(data.rollup.experiments.closed, locale),
134
- draft: formatCount(data.rollup.experiments.draft, locale),
135
- raw: formatCount(data.rollup.raw, locale),
136
- qualified:
137
- data.rollup.qualified === null
138
- ? t("common.unavailable")
139
- : formatCount(data.rollup.qualified, locale),
140
- rate: formatRate(data.rollup.qualificationRate, locale),
141
- ...(data.scope === "all"
142
- ? { sites: formatCount(data.rollup.sites, locale) }
143
- : {}),
144
- }}
145
- />
146
-
147
- <ExperimentsTable
148
- locale={locale}
149
- rows={rows}
150
- labels={{
151
- caption: t("list.table.caption"),
152
- columnKey: t("list.table.columnKey"),
153
- columnMode: t("list.table.columnMode"),
154
- columnStatus: t("list.table.columnStatus"),
155
- columnWaitlists: t("list.table.columnWaitlists"),
156
- columnRaw: t("list.table.columnRaw"),
157
- columnQualified: t("list.table.columnQualified"),
158
- columnRate: t("list.table.columnRate"),
159
- columnUpdated: t("list.table.columnUpdated"),
160
- emptyHeadline: t("list.table.emptyHeadline"),
161
- emptyDescription: t("list.table.emptyDescription"),
162
- statusLabel: (status) => statusLabels[status as WaitlistStatus],
163
- modeLabel: (mode) => modeLabels[mode as WaitlistMode],
164
- }}
165
- />
166
-
167
- {offerFunnels.map((funnel) => (
168
- <OfferFunnelTable
169
- key={funnel.experimentId}
170
- experimentId={funnel.experimentId}
171
- experimentKey={funnel.experimentKey}
172
- rows={funnel.rows}
173
- labels={offerLabels}
174
- />
175
- ))}
176
- </Container>
177
- </main>
178
- );
179
- }
@@ -1,178 +0,0 @@
1
- /**
2
- * /blog catch-all route. With `content/blog/` empty, the index renders the
3
- * BLOG_EMPTY_SENTINEL placeholder (testable contract).
4
- *
5
- * Q5 deferred post-MVP: no `/docs` companion ships in Phase 0 — only the
6
- * `docsPageJsonLd` builder is exported from `@working-theory/fumadocs-schema`.
7
- */
8
- import { blogListJsonLd, blogPostingJsonLd } from "@working-theory/fumadocs-schema";
9
- import { JsonLdScript, emptyStateVisible } from "@working-theory/page-renderer";
10
- import { BlogList, BlogPost } from "@working-theory/ui";
11
- import type { Metadata } from "next";
12
- import { notFound } from "next/navigation";
13
- import { getTranslations } from "next-intl/server";
14
- import type { ComponentType, ReactElement } from "react";
15
-
16
- import { toBlogListData } from "../../../../lib/blog-list-data";
17
- import { toBlogPostData } from "../../../../lib/blog-post-data";
18
- import { getOgImageUrl } from "../../../../lib/og";
19
- import { blogSource as defaultBlogSource } from "../../../../lib/source";
20
-
21
- /**
22
- * Sentinel string the empty-state placeholder must contain. Tests assert
23
- * on this exact value to detect the empty-blog branch without coupling to
24
- * copy. Exported so the integration test can import it directly.
25
- */
26
- export const BLOG_EMPTY_SENTINEL = "ailk-blog-empty-placeholder";
27
-
28
- /**
29
- * Frontmatter contract — must match `source.config.ts` `defineDocs.docs.schema`.
30
- */
31
- interface BlogFrontmatter {
32
- title: string;
33
- description: string;
34
- date?: string;
35
- updated?: string;
36
- image?: string;
37
- author?: { name: string; avatar?: string };
38
- tags?: string[];
39
- }
40
-
41
- /**
42
- * Post `page.data`: frontmatter plus the Fumadocs-generated `body` (compiled
43
- * MDX component) and `toc` (table of contents). The body renders through the
44
- * `BlogPost` section's `children` slot (issue #2714).
45
- */
46
- interface BlogPostPageData extends BlogFrontmatter {
47
- body?: ComponentType;
48
- toc?: Array<{ title: unknown; url: string; depth: number }>;
49
- }
50
-
51
- /**
52
- * Minimal blog-source interface covering the subset consumed by this route.
53
- * Tests inject a stub without mocking lib/source at the module level.
54
- */
55
- interface BlogSource {
56
- getPages: () => Array<{ url: string; data: unknown }>;
57
- getPage: (slug: string[]) => { data: unknown } | undefined;
58
- }
59
-
60
- interface BlogRouteProps {
61
- params: Promise<{ locale: string; slug?: string[] }>;
62
- /**
63
- * Blog source override for testing. Defaults to the real Fumadocs blogSource.
64
- * Inject a stub to avoid mocking lib/source at the module level.
65
- */
66
- blogSource?: BlogSource;
67
- }
68
-
69
- export async function generateMetadata(
70
- props: BlogRouteProps,
71
- ): Promise<Metadata> {
72
- const { slug } = await props.params;
73
- const source = props.blogSource ?? defaultBlogSource;
74
-
75
- if (slug && slug.length > 0) {
76
- const page = source.getPage(slug);
77
- if (page) {
78
- const data = page.data as unknown as BlogFrontmatter;
79
- const ogImageUrl = getOgImageUrl({
80
- title: data.title,
81
- schemaType: "BlogPosting",
82
- });
83
- return {
84
- openGraph: { images: [ogImageUrl] },
85
- twitter: { images: [ogImageUrl] },
86
- };
87
- }
88
- }
89
-
90
- const ogImageUrl = getOgImageUrl({ title: "Blog", schemaType: "BlogPosting" });
91
- return {
92
- openGraph: { images: [ogImageUrl] },
93
- twitter: { images: [ogImageUrl] },
94
- };
95
- }
96
-
97
- export default async function BlogRoute(
98
- props: BlogRouteProps,
99
- ): Promise<ReactElement> {
100
- const { locale, slug } = await props.params;
101
- const t = await getTranslations("blog");
102
- const source = props.blogSource ?? defaultBlogSource;
103
-
104
- if (slug && slug.length > 0) {
105
- const page = source.getPage(slug);
106
- if (!page) notFound();
107
-
108
- // Fumadocs' `PageData` is opaque; the frontmatter contract is enforced
109
- // by the `defineDocs` schema in source.config.ts at build time.
110
- const data = page.data as unknown as BlogPostPageData;
111
- const jsonLd = blogPostingJsonLd({
112
- slug: slug.join("/"),
113
- title: data.title,
114
- description: data.description,
115
- locale,
116
- ...(data.date !== undefined && { publishedAt: data.date }),
117
- ...(data.updated !== undefined && { updatedAt: data.updated }),
118
- ...(data.image !== undefined && { image: data.image }),
119
- });
120
-
121
- // The compiled MDX body renders through the section's `children` slot so it
122
- // keeps full formatting; the section maps the rest of the post (#2714).
123
- const MDXBody = data.body;
124
-
125
- return (
126
- <>
127
- <JsonLdScript data={jsonLd} />
128
- <main>
129
- <BlogPost sectionName="blog-post" data={toBlogPostData(data)}>
130
- {MDXBody ? <MDXBody /> : null}
131
- </BlogPost>
132
- </main>
133
- </>
134
- );
135
- }
136
-
137
- const pages = source.getPages();
138
-
139
- const indexJsonLd = blogListJsonLd({
140
- slug: "blog",
141
- title: "Blog",
142
- description: "Writing on AEO and AX-first software.",
143
- locale,
144
- });
145
-
146
- if (pages.length === 0) {
147
- // #4154: uniform with every other contentless surface — the empty-blog
148
- // placeholder is an authoring affordance, so it renders outside production
149
- // and 404s inside it rather than serving a stock page to a visitor.
150
- if (!emptyStateVisible()) notFound();
151
-
152
- return (
153
- <>
154
- <JsonLdScript data={indexJsonLd} />
155
- <main>
156
- <h1>{t("heading")}</h1>
157
- <p
158
- data-testid="blog-empty-placeholder"
159
- data-sentinel={BLOG_EMPTY_SENTINEL}
160
- >
161
- {t("emptyState")} <code>{"content/blog/"}</code>{" "}
162
- {t("emptyStateTrailing")}
163
- </p>
164
- </main>
165
- </>
166
- );
167
- }
168
-
169
- return (
170
- <>
171
- <JsonLdScript data={indexJsonLd} />
172
- <main>
173
- <h1>{t("heading")}</h1>
174
- <BlogList sectionName="blog-list" data={toBlogListData(pages)} />
175
- </main>
176
- </>
177
- );
178
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * Tests for purchaseAction Server Action.
3
- *
4
- * Acceptance criteria verified here:
5
- * - purchaseAction(validInput) returns { url: string } on success
6
- * - purchaseAction(invalidInput) returns Zod validation errors
7
- * - purchaseAction calls @working-theory/api-client checkout helper
8
- * - Parity shape: purchaseAction returns the same response shape as ailkPurchase
9
- */
10
-
11
- import { purchaseAction } from "../purchase.actions.js";
12
-
13
- // Mock @working-theory/api-client
14
- jest.mock("@working-theory/api-client", () => ({
15
- checkout: {
16
- createSession: jest.fn(),
17
- },
18
- }));
19
-
20
- import { checkout } from "@working-theory/api-client";
21
- const mockCreateSession = checkout.createSession as jest.Mock;
22
-
23
- afterEach(() => {
24
- jest.clearAllMocks();
25
- });
26
-
27
- describe("purchaseAction — valid input", () => {
28
- it("returns { url } on success", async () => {
29
- mockCreateSession.mockResolvedValueOnce({
30
- ok: true,
31
- body: {
32
- sessionId: "cs_test_abc123",
33
- url: "https://checkout.stripe.com/pay/cs_test_abc123",
34
- },
35
- });
36
-
37
- const result = await purchaseAction({
38
- priceId: "price_test_123",
39
- quantity: 1,
40
- successUrl: "https://example.com/success",
41
- cancelUrl: "https://example.com/cancel",
42
- });
43
-
44
- expect(result).toEqual({
45
- url: "https://checkout.stripe.com/pay/cs_test_abc123",
46
- });
47
- });
48
-
49
- it("returns { url } when quantity is omitted", async () => {
50
- mockCreateSession.mockResolvedValueOnce({
51
- ok: true,
52
- body: {
53
- sessionId: "cs_test_xyz",
54
- url: "https://checkout.stripe.com/pay/cs_test_xyz",
55
- },
56
- });
57
-
58
- const result = await purchaseAction({
59
- priceId: "price_test_abc",
60
- successUrl: "https://example.com/success",
61
- cancelUrl: "https://example.com/cancel",
62
- });
63
-
64
- expect(result).toEqual({
65
- url: "https://checkout.stripe.com/pay/cs_test_xyz",
66
- });
67
- });
68
-
69
- it("calls checkout.createSession with mapped payload", async () => {
70
- mockCreateSession.mockResolvedValueOnce({
71
- ok: true,
72
- body: {
73
- sessionId: "cs_test_def",
74
- url: "https://checkout.stripe.com/pay/cs_test_def",
75
- },
76
- });
77
-
78
- await purchaseAction({
79
- priceId: "price_plan_pro",
80
- quantity: 2,
81
- successUrl: "https://example.com/success",
82
- cancelUrl: "https://example.com/cancel",
83
- });
84
-
85
- expect(mockCreateSession).toHaveBeenCalledWith({
86
- productCode: "price_plan_pro",
87
- quantity: 2,
88
- successUrl: "https://example.com/success",
89
- cancelUrl: "https://example.com/cancel",
90
- });
91
- });
92
- });
93
-
94
- describe("purchaseAction — invalid input (Zod errors)", () => {
95
- it("returns validation errors when priceId is missing", async () => {
96
- const result = await purchaseAction({
97
- successUrl: "https://example.com/success",
98
- cancelUrl: "https://example.com/cancel",
99
- } as never);
100
-
101
- expect(result).toHaveProperty("errors");
102
- expect(mockCreateSession).not.toHaveBeenCalled();
103
- });
104
-
105
- it("returns validation errors when priceId is empty", async () => {
106
- const result = await purchaseAction({
107
- priceId: "",
108
- successUrl: "https://example.com/success",
109
- cancelUrl: "https://example.com/cancel",
110
- });
111
-
112
- expect(result).toHaveProperty("errors");
113
- expect(mockCreateSession).not.toHaveBeenCalled();
114
- });
115
-
116
- it("returns validation errors when priceId contains invalid characters", async () => {
117
- // priceId must be min length 1; test with a non-string
118
- const result = await purchaseAction({
119
- // eslint-disable-next-line no-restricted-syntax -- Phase 2/3 follow-up: pre-existing test-hygiene violation
120
- priceId: 12345 as unknown as string,
121
- successUrl: "https://example.com/success",
122
- cancelUrl: "https://example.com/cancel",
123
- });
124
-
125
- expect(result).toHaveProperty("errors");
126
- expect(mockCreateSession).not.toHaveBeenCalled();
127
- });
128
-
129
- it("returns validation errors when quantity is not a positive integer", async () => {
130
- const result = await purchaseAction({
131
- priceId: "price_test",
132
- quantity: 0,
133
- successUrl: "https://example.com/success",
134
- cancelUrl: "https://example.com/cancel",
135
- });
136
-
137
- expect(result).toHaveProperty("errors");
138
- expect(mockCreateSession).not.toHaveBeenCalled();
139
- });
140
- });
141
-
142
- describe("purchaseAction — parity shape with ailkPurchase", () => {
143
- it("success shape matches ailkPurchase success shape: { url: string }", async () => {
144
- mockCreateSession.mockResolvedValueOnce({
145
- ok: true,
146
- body: {
147
- sessionId: "cs_test_parity",
148
- url: "https://checkout.stripe.com/pay/cs_test_parity",
149
- },
150
- });
151
-
152
- const result = await purchaseAction({
153
- priceId: "price_parity",
154
- successUrl: "https://example.com/success",
155
- cancelUrl: "https://example.com/cancel",
156
- });
157
-
158
- expect(typeof (result as { url: string }).url).toBe("string");
159
- });
160
- });
@@ -1,25 +0,0 @@
1
- "use client";
2
- // Per-segment error boundary for the /dev/purchase (checkout) route.
3
- // Does not make any claim about whether payment was processed.
4
- import type { ReactElement } from "react";
5
-
6
- import { ErrorFallback } from "../../../error-fallback";
7
-
8
- interface ErrorProps {
9
- error: Error & { digest?: string };
10
- reset: () => void;
11
- }
12
-
13
- export default function PurchaseError({
14
- error,
15
- reset,
16
- }: ErrorProps): ReactElement {
17
- return (
18
- <ErrorFallback
19
- error={error}
20
- reset={reset}
21
- heading="Checkout couldn't complete"
22
- body="Something interrupted your purchase attempt. Please try again to continue."
23
- />
24
- );
25
- }
@@ -1,117 +0,0 @@
1
- /* eslint-disable no-restricted-syntax -- i18n-allow: dev-only route, not in customer navigation (ADR 0019) */
2
- "use client";
3
-
4
- /**
5
- * /dev/purchase — demo route for the purchaseAction Server Action.
6
- *
7
- * SD-3d: /dev/-prefixed routes are not in customer-visible navigation.
8
- * This page exists to exercise the Server Action / MCP tool parity pair
9
- * in a browser context during Phase 3 development.
10
- */
11
-
12
- import { useState } from "react";
13
-
14
- import { purchaseAction } from "./purchase.actions";
15
- import type { PurchaseActionResult } from "./purchase.actions";
16
-
17
- export default function DevPurchasePage() {
18
- const [priceId, setPriceId] = useState("price_test_123");
19
- const [result, setResult] = useState<PurchaseActionResult | null>(null);
20
- const [pending, setPending] = useState(false);
21
-
22
- async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
23
- e.preventDefault();
24
- setPending(true);
25
- setResult(null);
26
- try {
27
- const res = await purchaseAction({ priceId });
28
- setResult(res);
29
- if ("url" in res) {
30
- window.location.href = res.url;
31
- }
32
- } finally {
33
- setPending(false);
34
- }
35
- }
36
-
37
- return (
38
- <main style={{ padding: "2rem", maxWidth: 480 }}>
39
- <h1>Dev: Purchase (Server Action smoke test)</h1>
40
- <p style={{ fontSize: "0.875rem", color: "#666" }}>
41
- /dev/ — not in customer navigation
42
- </p>
43
-
44
- <form
45
- onSubmit={(e) => void handleSubmit(e)}
46
- style={{
47
- display: "flex",
48
- flexDirection: "column",
49
- gap: "1rem",
50
- marginTop: "1.5rem",
51
- }}
52
- >
53
- <label>
54
- <span
55
- style={{
56
- display: "block",
57
- marginBottom: "0.25rem",
58
- fontWeight: 500,
59
- }}
60
- >
61
- Price ID
62
- </span>
63
- <input
64
- type="text"
65
- value={priceId}
66
- onChange={(e) => setPriceId(e.target.value)}
67
- placeholder="price_test_..."
68
- required
69
- style={{
70
- width: "100%",
71
- padding: "0.5rem",
72
- border: "1px solid #ccc",
73
- borderRadius: 4,
74
- }}
75
- />
76
- </label>
77
-
78
- <button
79
- type="submit"
80
- disabled={pending}
81
- style={{
82
- padding: "0.5rem 1rem",
83
- background: "#0070f3",
84
- color: "#fff",
85
- border: "none",
86
- borderRadius: 4,
87
- cursor: pending ? "not-allowed" : "pointer",
88
- }}
89
- >
90
- {pending ? "Creating session…" : "Start checkout"}
91
- </button>
92
- </form>
93
-
94
- {result && (
95
- <div style={{ marginTop: "1.5rem" }}>
96
- {"errors" in result ? (
97
- <div>
98
- <strong>Validation errors:</strong>
99
- <ul>
100
- {result.errors.map((e, i) => (
101
- <li key={i}>
102
- {e.path.join(".") || "input"}: {e.message}
103
- </li>
104
- ))}
105
- </ul>
106
- </div>
107
- ) : (
108
- <div>
109
- <strong>Checkout URL:</strong>{" "}
110
- <a href={result.url}>{result.url}</a>
111
- </div>
112
- )}
113
- </div>
114
- )}
115
- </main>
116
- );
117
- }