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,426 @@
1
+ /**
2
+ * @file project-listing-flow.ts
3
+ * @description The approve-time projection (#5001): a `ProjectListing` row's
4
+ * `questions` block becomes a live diagnostic — one `FlowDefinition` the
5
+ * visitor walks at `/v1/flows/{slug}` and one `WaitlistExperiment` keyed
6
+ * `{slug}` that scores the walk through the shared scoring rule
7
+ * (`@working-theory/database/flows/listing-diagnostic`).
8
+ *
9
+ * `generateListingFlow` writes through the two existing validated paths —
10
+ * `upsertFlowDefinition` (`flow-engine.ts`) and `upsertWaitlistExperiment`
11
+ * (`waitlist-experiments.ts`) — never a second ad-hoc Prisma write, and
12
+ * never `seedWaitlistExperiments`/`materializeWaitlistFlows`. Both underlying
13
+ * writes upsert on `[siteId, slug]` / `[siteId, key]`, and the `experimentKey`
14
+ * write-back is the same value every time, so re-approving a listing is a
15
+ * no-op rather than a duplicate.
16
+ *
17
+ * ONE vocabulary for the scored questions, since #5036 D4: the flow renders
18
+ * the founder's own option LABELS under the founder's own option VALUES, the
19
+ * register keys its `optionWeights` on those same values
20
+ * (`buildListingDiagnosticScoring`), and the experiment's `questionSet`
21
+ * declares that same per-listing option list — which is what satisfies
22
+ * `waitlistExperimentConfigSchema`'s "every weights key must be a declared
23
+ * option of the question it names" cross-validation. The old
24
+ * `q{n}:{i}of{k}` positional token, and the finite value universe it forced
25
+ * the question set to declare, are both gone. Only the EXTRAS keep a
26
+ * positional encoding (`x1:0of3`); they are unscored, so nothing resolves
27
+ * them by value.
28
+ *
29
+ * The tier NEVER reaches the visitor: the flow's terminal `message` step
30
+ * carries the confirmation line and the founder's own one-liner, and the
31
+ * config declares no `thankYou` / `deliverables` block, so nothing on the
32
+ * visitor's path can render a scoring outcome.
33
+ *
34
+ * PII boundary: logs and returned errors carry only `{ siteId, listingId,
35
+ * slug }` — never `founderEmail`, `founderName`, or `questions` (the same
36
+ * discipline `project-listings.ts` and `lead-routing.ts` document).
37
+ */
38
+
39
+ import { buildListingDiagnosticScoring } from "@working-theory/database/flows/listing-diagnostic";
40
+ import type {
41
+ FlowConfig,
42
+ FlowStep,
43
+ ProjectListingQuestions,
44
+ WaitlistQuestion,
45
+ } from "@working-theory/validation";
46
+
47
+ import { prisma } from "../lib/prisma.js";
48
+
49
+ import { upsertFlowDefinition } from "./flow-engine.js";
50
+ import { upsertWaitlistExperiment } from "./waitlist-experiments.js";
51
+
52
+ // ─── Types ────────────────────────────────────────────────────────────────────
53
+
54
+ /** The subset of the row the projection reads. */
55
+ export type ListingFlowSource = {
56
+ id: string;
57
+ siteId: string;
58
+ slug: string;
59
+ name: string;
60
+ oneLiner: string;
61
+ questions: ProjectListingQuestions;
62
+ };
63
+
64
+ export type GenerateListingFlowError = {
65
+ kind: "invalid_input" | "flows_not_configured";
66
+ message: string;
67
+ };
68
+
69
+ export type GenerateListingFlowResult =
70
+ | { ok: true; flowSlug: string; experimentKey: string }
71
+ | { ok: false; error: GenerateListingFlowError };
72
+
73
+ const DEFAULT_LOCALE = "en";
74
+ const en = (s: string) => ({ [DEFAULT_LOCALE]: s });
75
+
76
+ /** At most three extras (`projectListingQuestionsSchema`). */
77
+ type ExtraIndex = 1 | 2 | 3;
78
+
79
+ // ─── Identifier derivation ──────────────────────────────────────────────────
80
+
81
+ /**
82
+ * The register-internal waitlist key for a listing slug. A listing slug is
83
+ * kebab AND may start with a digit (`^[a-z0-9-]{3,48}$`, #4998), while
84
+ * `flowIdentifierSchema` demands `^[a-z][a-z0-9_]*$` — so the `listing_`
85
+ * prefix is load-bearing, not decoration. 8 + 48 = 56 ≤ the 64-char cap.
86
+ */
87
+ export function listingWaitlistKey(slug: string): string {
88
+ return `listing_${slug.replace(/-/g, "_")}`;
89
+ }
90
+
91
+ /** `extra_1` … `extra_3` — the flow field name and question id for one extra. */
92
+ function extraFieldName(index: ExtraIndex): string {
93
+ return `extra_${index}`;
94
+ }
95
+
96
+ /**
97
+ * An extra's option value — `x1:0of3`. Extras carry no points and no
98
+ * register resolves them, so the positional encoding survives #5036 D4's move
99
+ * to value-keyed scoring: nothing depends on an extra's value MEANING
100
+ * anything, only on it being stable and listing-independent for the CSV and
101
+ * any later reader.
102
+ */
103
+ function extraOptionValue(index: ExtraIndex, optionIndex: number, arity: number): string {
104
+ return `x${index}:${optionIndex}of${arity}`;
105
+ }
106
+
107
+ // ─── The flow (what the visitor walks) ──────────────────────────────────────
108
+
109
+ /**
110
+ * One option as every optioned question declares it — the founder's own
111
+ * `{ value, label }` pair. Since #5036 D4 the scored Q3–Q6 carry exactly the
112
+ * same shape as the unscored Q2: no positional rewrite in between.
113
+ */
114
+ type OptionPair = { value: string; label: string };
115
+
116
+ /** One single-select `field` step — the shape every diagnostic question takes. */
117
+ function selectStep(
118
+ key: string,
119
+ fieldName: string,
120
+ prompt: string,
121
+ options: readonly OptionPair[],
122
+ ): FlowStep {
123
+ return {
124
+ key,
125
+ type: "field",
126
+ copy: { title: en(prompt) },
127
+ fields: [
128
+ {
129
+ name: fieldName,
130
+ inputType: "select",
131
+ required: true,
132
+ options: options.map((o) => ({ value: o.value, label: en(o.label) })),
133
+ copy: { label: en(prompt) },
134
+ },
135
+ ],
136
+ };
137
+ }
138
+
139
+ /**
140
+ * The six-category step order (#5001): contact → segment → problem → fit →
141
+ * urgency → price → open → extras, then the confirmation.
142
+ */
143
+ function buildFlowSteps(listing: ListingFlowSource): FlowStep[] {
144
+ const { questions } = listing;
145
+
146
+ const steps: FlowStep[] = [
147
+ {
148
+ key: "contact",
149
+ type: "field",
150
+ copy: { title: en(`Join the ${listing.name} waitlist`) },
151
+ fields: [
152
+ {
153
+ name: "name",
154
+ inputType: "text",
155
+ mapsTo: "name",
156
+ copy: { label: en("Name") },
157
+ },
158
+ {
159
+ name: "email",
160
+ inputType: "email",
161
+ required: true,
162
+ mapsTo: "email",
163
+ copy: { label: en("Email") },
164
+ },
165
+ ],
166
+ },
167
+ // Every optioned question — the unscored Q2 (including the fixed
168
+ // `just-curious` bail-out its schema pins last) and the scored Q3–Q6
169
+ // alike — renders under the founder's OWN option values, so the raw value
170
+ // in the session state the weekly CSV reads is the same value the
171
+ // register scores.
172
+ selectStep("segment", "q2", questions.q2.prompt, questions.q2.options),
173
+ selectStep("problem", "q3", questions.q3.prompt, questions.q3.options),
174
+ selectStep("fit", "q4", questions.q4.prompt, questions.q4.options),
175
+ selectStep("urgency", "q5", questions.q5.prompt, questions.q5.options),
176
+ selectStep("price", "q6", questions.q6.prompt, questions.q6.options),
177
+ {
178
+ key: "open",
179
+ type: "field",
180
+ copy: { title: en(questions.q7.prompt) },
181
+ fields: [
182
+ {
183
+ name: "q7",
184
+ inputType: "textarea",
185
+ copy: { label: en(questions.q7.prompt) },
186
+ },
187
+ ],
188
+ },
189
+ ];
190
+
191
+ if (questions.extras.length > 0) {
192
+ steps.push({
193
+ key: "extras",
194
+ type: "field",
195
+ copy: { title: en("A few more questions") },
196
+ fields: questions.extras.map((extra, i) => {
197
+ const index = (i + 1) as ExtraIndex;
198
+ return {
199
+ name: extraFieldName(index),
200
+ inputType: "select" as const,
201
+ required: true,
202
+ options: extra.options.map((option, optionIndex) => ({
203
+ value: extraOptionValue(index, optionIndex, extra.options.length),
204
+ label: en(option.label),
205
+ })),
206
+ copy: { label: en(extra.prompt) },
207
+ };
208
+ }),
209
+ });
210
+ }
211
+
212
+ // Confirmation only — the founder's own line, never the tier.
213
+ steps.push({
214
+ key: "done",
215
+ type: "message",
216
+ copy: {
217
+ title: en(`You're on the ${listing.name} waitlist`),
218
+ ...(listing.oneLiner.trim().length > 0 && { body: en(listing.oneLiner) }),
219
+ },
220
+ });
221
+
222
+ return steps;
223
+ }
224
+
225
+ function buildFlowConfig(listing: ListingFlowSource): FlowConfig {
226
+ return {
227
+ slug: listing.slug,
228
+ defaultLocale: DEFAULT_LOCALE,
229
+ steps: buildFlowSteps(listing),
230
+ experiment: {
231
+ key: listing.slug,
232
+ waitlist: listingWaitlistKey(listing.slug),
233
+ },
234
+ };
235
+ }
236
+
237
+ // ─── The experiment (what scores the walk) ──────────────────────────────────
238
+
239
+ /**
240
+ * The register's question set — every question mirrors the flow exactly,
241
+ * Q3–Q6 included: since #5036 D4 the register scores this listing's OWN
242
+ * option values, so the question set declares this listing's own options and
243
+ * the config-level cross-validation passes without a value universe.
244
+ */
245
+ function buildQuestionSet(listing: ListingFlowSource): WaitlistQuestion[] {
246
+ const { questions } = listing;
247
+
248
+ const scored = (["q3", "q4", "q5", "q6"] as const).map((id) => ({
249
+ id,
250
+ prompt: en(questions[id].prompt),
251
+ answerType: "single" as const,
252
+ options: questions[id].options.map((o) => ({ value: o.value, label: en(o.label) })),
253
+ }));
254
+
255
+ return [
256
+ {
257
+ id: "name",
258
+ prompt: en("Name"),
259
+ answerType: "text" as const,
260
+ required: false,
261
+ mapsTo: "name" as const,
262
+ },
263
+ {
264
+ id: "email",
265
+ prompt: en("Email"),
266
+ answerType: "email" as const,
267
+ mapsTo: "email" as const,
268
+ },
269
+ {
270
+ id: "q2",
271
+ prompt: en(questions.q2.prompt),
272
+ answerType: "single" as const,
273
+ options: questions.q2.options.map((o) => ({ value: o.value, label: en(o.label) })),
274
+ },
275
+ ...scored,
276
+ {
277
+ id: "q7",
278
+ prompt: en(questions.q7.prompt),
279
+ answerType: "text" as const,
280
+ required: false,
281
+ },
282
+ ...questions.extras.map((extra, i) => {
283
+ const index = (i + 1) as ExtraIndex;
284
+ return {
285
+ id: extraFieldName(index),
286
+ prompt: en(extra.prompt),
287
+ answerType: "single" as const,
288
+ options: extra.options.map((option, optionIndex) => ({
289
+ value: extraOptionValue(index, optionIndex, extra.options.length),
290
+ label: en(option.label),
291
+ })),
292
+ };
293
+ }),
294
+ ];
295
+ }
296
+
297
+ function buildExperimentConfig(listing: ListingFlowSource): unknown {
298
+ const waitlistKey = listingWaitlistKey(listing.slug);
299
+ return {
300
+ key: listing.slug,
301
+ mode: "qualifying",
302
+ status: "live",
303
+ waitlists: [{ key: waitlistKey, flowSlug: listing.slug }],
304
+ questionSet: { version: 1, questions: buildQuestionSet(listing) },
305
+ // The shared scoring RULE, applied to this listing's own option values
306
+ // — same point names, same total, same tier floors for every listing;
307
+ // only the Q3/Q4 weight keys differ, because those keys are the founder's
308
+ // own values (#5036 D4). Embedded verbatim:
309
+ // `waitlistExperimentConfigSchema` carries scoring by value and declares
310
+ // no by-reference slot.
311
+ scoring: buildListingDiagnosticScoring(listing.questions),
312
+ tracking: {
313
+ version: 1,
314
+ utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
315
+ waitlists: { [waitlistKey]: { campaign: listing.slug, channels: ["direct"] } },
316
+ },
317
+ stripeConfigRef: null,
318
+ };
319
+ }
320
+
321
+ // ─── The inverse (what the weekly CSV reads) ────────────────────────────────
322
+
323
+ /**
324
+ * `field name -> option value -> the founder's own label`, for every optioned
325
+ * question the projection emits. The weekly CSV (`listing-csv-sweep.ts`)
326
+ * renders a signup's stored answer through this so the founder reads their own
327
+ * wording rather than a bare value.
328
+ *
329
+ * Q2–Q6 collapse to a plain value→label map since #5036 D4: the flow renders
330
+ * the founder's own values, so the inverse is the founder's own option list
331
+ * read the other way round. Only the extras still need the positional
332
+ * `extraOptionValue` — derived from the SAME call the projection emits, so
333
+ * the two can never disagree about what a stored value means.
334
+ */
335
+ export function listingAnswerLabels(
336
+ questions: ProjectListingQuestions,
337
+ ): Record<string, Record<string, string>> {
338
+ const labels: Record<string, Record<string, string>> = {};
339
+
340
+ for (const question of ["q2", "q3", "q4", "q5", "q6"] as const) {
341
+ labels[question] = Object.fromEntries(
342
+ questions[question].options.map((option) => [option.value, option.label]),
343
+ );
344
+ }
345
+
346
+ questions.extras.forEach((extra, i) => {
347
+ const index = (i + 1) as ExtraIndex;
348
+ labels[extraFieldName(index)] = Object.fromEntries(
349
+ extra.options.map((option, optionIndex) => [
350
+ extraOptionValue(index, optionIndex, extra.options.length),
351
+ option.label,
352
+ ]),
353
+ );
354
+ });
355
+
356
+ return labels;
357
+ }
358
+
359
+ // ─── generateListingFlow ────────────────────────────────────────────────────
360
+
361
+ /**
362
+ * Project an approved listing into its live diagnostic. Idempotent: both
363
+ * writes upsert on their natural key and the `experimentKey` write-back is
364
+ * stable, so a second approve changes nothing.
365
+ *
366
+ * Both joins are stated rather than inferred (#5036 D6d). The definition
367
+ * carries the listing's id, so a session started on this flow names the
368
+ * listing directly instead of relying on the flow's slug matching it; and the
369
+ * listing stores the experiment's KEY, which is the string `WaitlistScore`
370
+ * itself holds, so reading a listing's submissions is one hop rather than an
371
+ * id-to-key lookup. ADR 0009 forbids a relation across the namespace
372
+ * boundary, so both are plain strings — which is exactly why each has to be
373
+ * the string the other side actually stores.
374
+ *
375
+ * The FLOW is written first: an experiment row whose `waitlists[].flowSlug`
376
+ * resolves to nothing would leave `GET /v1/flows/{slug}` answering
377
+ * `flow_not_found` while the dashboard already showed a live experiment. The
378
+ * reverse ordering (a servable flow whose experiment has not landed yet)
379
+ * degrades to "collected, not yet scored" — recoverable on the next approve.
380
+ */
381
+ export async function generateListingFlow(
382
+ listing: ListingFlowSource,
383
+ ): Promise<GenerateListingFlowResult> {
384
+ const flow = await upsertFlowDefinition(
385
+ listing.siteId,
386
+ buildFlowConfig(listing),
387
+ listing.id,
388
+ );
389
+ if (!flow.ok) {
390
+ return {
391
+ ok: false,
392
+ error: {
393
+ kind: flow.error.kind === "flows_not_configured" ? "flows_not_configured" : "invalid_input",
394
+ message: flow.error.message,
395
+ },
396
+ };
397
+ }
398
+
399
+ const experiment = await upsertWaitlistExperiment(
400
+ listing.siteId,
401
+ buildExperimentConfig(listing),
402
+ );
403
+ if (!experiment.ok) {
404
+ return {
405
+ ok: false,
406
+ error: {
407
+ kind:
408
+ experiment.error.kind === "flows_not_configured"
409
+ ? "flows_not_configured"
410
+ : "invalid_input",
411
+ message: experiment.error.message,
412
+ },
413
+ };
414
+ }
415
+
416
+ // The experiment's key IS the listing's slug (`buildExperimentConfig`), and
417
+ // it is what `WaitlistScore.experimentKey` stores.
418
+ const experimentKey = listing.slug;
419
+
420
+ await prisma.projectListing.update({
421
+ where: { id: listing.id },
422
+ data: { experimentKey },
423
+ });
424
+
425
+ return { ok: true, flowSlug: listing.slug, experimentKey };
426
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * @file project-listing-issue.ts
3
+ * @description The approve-time GitHub issue filing (#5000): an approved
4
+ * `ProjectListing` becomes a work order filed on the SITE's own repo
5
+ * (`AILK_GITHUB_REPO`), in the shape that repo's issue-claiming pipeline
6
+ * reads (`.claude/skills/file-issue/references/issue.md`'s canonical Issue
7
+ * body — `## What` / `## Why` / `## Acceptance Criteria` / `## Scope` /
8
+ * `## Grounding` / the `<!-- dep-graph: -->` block).
9
+ *
10
+ * `renderListingIssue` is pure — no fetch, no Prisma, no config read — so
11
+ * the shape is unit-testable against a fixture row with no network double.
12
+ * `fileListingIssue` is the effectful half: skip (env absent, or `issueUrl`
13
+ * already set) -> POST -> write `issueUrl` back on 201 -> log + return on
14
+ * ANY failure. It never throws; `project-listing-decision.ts`'s
15
+ * `sendListingDecisionEmail` calls it fire-and-forget after the founder
16
+ * email, with its own `.catch` boundary on top of the try/catch below, so a
17
+ * filing failure can never reach the approve path that has already
18
+ * committed.
19
+ *
20
+ * Injection hardening, mirroring `slack-notify.ts`'s `escapeMrkdwn` for the
21
+ * same class of anonymous-input risk (PR #4999 security review): every
22
+ * founder-authored scalar field (name/oneLiner/audience/problem/founderName/
23
+ * founderCompany/url) is (1) collapsed to a single line — an embedded
24
+ * newline could otherwise open a line with `## ` and forge a section a
25
+ * naive line-based consumer parser reads as real (e.g. a fake `## Scope`
26
+ * re-pointing what a worker builds), and (2) rendered as inline code, which
27
+ * GitHub never parses for an `@mention` or `#issue` auto-link. The
28
+ * `questions` block is embedded as fenced JSON instead — `JSON.stringify`
29
+ * already escapes any embedded newline to the two-character `\n` sequence,
30
+ * so no extra sanitization is needed there, and it stays byte-for-byte the
31
+ * stored value (#5000 AC2 — "the questions block verbatim").
32
+ *
33
+ * PII boundary: `founderEmail` lives on `ProjectListing` but is never
34
+ * referenced here — the row has it; this file never renders or logs it.
35
+ */
36
+ import { logger } from "@working-theory/observability";
37
+ import type { ProjectListing } from "@working-theory/validation";
38
+
39
+ import { config } from "../config/index.js";
40
+ import { prisma } from "../lib/prisma.js";
41
+
42
+ const GITHUB_API_BASE = "https://api.github.com";
43
+ const ISSUE_LABELS = ["status/ready"] as const;
44
+
45
+ const SCOPE_PATHS = [
46
+ "apps/web/app/[locale]/projects/[slug]/**",
47
+ "apps/web/src/content/projects/**",
48
+ "apps/web/src/content/diagnostic/**",
49
+ ] as const;
50
+
51
+ export type RenderListingIssueOptions = {
52
+ /** `owner/name` of the repo the issue is filed ON — the site's own repo, never this one. */
53
+ targetRepo: string;
54
+ };
55
+
56
+ export type RenderedListingIssue = {
57
+ title: string;
58
+ body: string;
59
+ labels: string[];
60
+ };
61
+
62
+ /**
63
+ * Collapses an embedded line ending (CR and/or LF) to a space — see the
64
+ * module doc's injection note — and strips backticks. Without stripping
65
+ * backticks, a founder-authored value containing one (e.g. `` Bob` @someone ``)
66
+ * closes `code()`'s single-backtick span early, leaving everything after it
67
+ * as live, unfenced markdown GitHub WILL autolink into a real `@mention`
68
+ * (PR #5011 security review, Finding 1) — the same failure mode `escapeMrkdwn`
69
+ * exists to prevent for Slack, just triggered by a different delimiter.
70
+ * A backtick is not meaningful content in any of these fields (a name, a
71
+ * one-liner, a URL, …), so substituting a straight quote is a lossless-enough
72
+ * trade for closing the injection outright, rather than computing a
73
+ * CommonMark-safe variable-length fence.
74
+ */
75
+ function singleLine(value: string): string {
76
+ return value.replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim();
77
+ }
78
+
79
+ /** What an absent field renders as. */
80
+ const ABSENT = "—";
81
+
82
+ /**
83
+ * Inline code: verbatim AND immune to GitHub's `@mention`/`#issue` markdown
84
+ * parsing.
85
+ *
86
+ * `null` is accepted because every application field became nullable at rest
87
+ * in #5036 D1 — a half-filled draft is a legitimate row. An issue is only ever
88
+ * filed from an APPROVED listing, which `submit` proved complete, so the null
89
+ * branch discharges a type-level possibility rather than a reachable state.
90
+ */
91
+ function code(value: string | null): string {
92
+ return value === null ? ABSENT : `\`${singleLine(value)}\``;
93
+ }
94
+
95
+ /**
96
+ * Renders the build-issue title/body/labels from an approved listing row.
97
+ * Pure — every value comes from `listing` and `targetRepo`, nothing is read
98
+ * from config or the network.
99
+ */
100
+ export function renderListingIssue(
101
+ listing: ProjectListing,
102
+ { targetRepo }: RenderListingIssueOptions,
103
+ ): RenderedListingIssue {
104
+ // `slug` is null only on a draft, and a draft never reaches this renderer.
105
+ const slug = listing.slug ?? listing.id;
106
+ const title = `feat(web): /projects/${slug} listing + diagnostic (listing ${listing.id})`;
107
+
108
+ const rowLines = [
109
+ `- id: ${code(listing.id)}`,
110
+ `- slug: ${code(listing.slug)}`,
111
+ `- name: ${code(listing.name)}`,
112
+ `- oneLiner: ${code(listing.oneLiner)}`,
113
+ `- problem: ${code(listing.problem)}`,
114
+ `- url: ${listing.url ? code(listing.url) : ABSENT}`,
115
+ `- product: ${code(listing.product)}`,
116
+ `- commitment: ${code(listing.commitment)}`,
117
+ `- teamComposition: ${code(listing.teamComposition)}`,
118
+ `- address: ${code(listing.address)}`,
119
+ `- anythingElse: ${listing.anythingElse ? code(listing.anythingElse) : ABSENT}`,
120
+ `- founderName: ${code(listing.founderName)}`,
121
+ `- founderCompany: ${listing.founderCompany ? code(listing.founderCompany) : ABSENT}`,
122
+ ].join("\n");
123
+
124
+ const questionsJson = JSON.stringify(listing.questions, null, 2);
125
+ // #5036 — the three FAQ entries the detail page renders. Emitted as FENCED
126
+ // JSON rather than as `- ` bullets, the same treatment `questions` already
127
+ // gets: `answer` is 1000 characters of anonymous free text with no
128
+ // control-character rule, and a bullet list is exactly the shape
129
+ // docs/lessons/project-listings/07 describes — anonymous text landing in a
130
+ // document a downstream tool parses by heading. Inside a fence it forges
131
+ // nothing.
132
+ const faqsJson = JSON.stringify(listing.faqs ?? [], null, 2);
133
+ const scopeLines = SCOPE_PATHS.map((p) => `- ${p}`).join("\n");
134
+
135
+ const body = `<!-- ailk-listing-issue: repo=${targetRepo} listing=${listing.id} -->
136
+
137
+ ## What
138
+
139
+ Build the public listing page and its diagnostic flow for ${code(listing.name)}:
140
+
141
+ - The page at \`/projects/${slug}\`, rendered from this row via the public read (\`GET /v1/project-listings/public\`).
142
+ - The diagnostic at \`/flows/${slug}\`, registered from the flow generated on approve.
143
+
144
+ **Row**
145
+
146
+ ${rowLines}
147
+
148
+ **FAQs**
149
+
150
+ \`\`\`\`json
151
+ ${faqsJson}
152
+ \`\`\`\`
153
+
154
+ **Questions**
155
+
156
+ \`\`\`\`json
157
+ ${questionsJson}
158
+ \`\`\`\`
159
+
160
+ ## Why
161
+
162
+ An approved application must be a complete work order a worker can build from without the operator touching it again.
163
+
164
+ ## Acceptance Criteria
165
+
166
+ - [ ] page renders from the row
167
+ - [ ] diagnostic flow live at /flows/${slug}
168
+ - [ ] waitlist joins land under the listing's experiment
169
+ - [ ] /projects tile appears in the D6 order
170
+
171
+ ## Scope
172
+
173
+ ${scopeLines}
174
+
175
+ ## Grounding
176
+
177
+ - **Governance:** \`internal\`
178
+ - **Nature:** \`feature\`
179
+ - **Resourced:** \`n/a\`
180
+
181
+ <!-- dep-graph:
182
+ blocked-by:
183
+ blocks:
184
+ priority:
185
+ scope-paths: ${SCOPE_PATHS.join(", ")}
186
+ -->
187
+ `;
188
+
189
+ return { title, body, labels: [...ISSUE_LABELS] };
190
+ }
191
+
192
+ /**
193
+ * Files the build issue for an approved listing, writing `issueUrl` back on
194
+ * success. No-op (`github-skipped`) when `AILK_GITHUB_REPO`/`AILK_GITHUB_TOKEN`
195
+ * are absent, or when the row already carries `issueUrl` (idempotent — a
196
+ * second approve, or a stray re-call, never double-files). Never throws: any
197
+ * failure (network, non-201, a malformed response) is logged — id + status
198
+ * only, never the token, never a response body that could carry one — and
199
+ * the row is left with `issueUrl: null` for a later retry via the PATCH
200
+ * route.
201
+ */
202
+ export async function fileListingIssue(listing: ProjectListing): Promise<void> {
203
+ const targetRepo = config.AILK_GITHUB_REPO;
204
+ const token = config.AILK_GITHUB_TOKEN;
205
+ if (!targetRepo || !token) {
206
+ logger.info({ listingId: listing.id }, "github-skipped");
207
+ return;
208
+ }
209
+ if (listing.issueUrl) {
210
+ return;
211
+ }
212
+
213
+ const { title, body, labels } = renderListingIssue(listing, { targetRepo });
214
+
215
+ try {
216
+ const res = await fetch(`${GITHUB_API_BASE}/repos/${targetRepo}/issues`, {
217
+ method: "POST",
218
+ headers: {
219
+ authorization: `Bearer ${token}`,
220
+ accept: "application/vnd.github+json",
221
+ "content-type": "application/json",
222
+ "X-GitHub-Api-Version": "2022-11-28",
223
+ },
224
+ body: JSON.stringify({ title, body, labels }),
225
+ });
226
+
227
+ if (res.status !== 201) {
228
+ logger.warn({ listingId: listing.id, status: res.status }, "listing issue filing failed");
229
+ return;
230
+ }
231
+
232
+ const json = (await res.json()) as { html_url?: unknown };
233
+ if (typeof json.html_url !== "string" || json.html_url.length === 0) {
234
+ logger.warn(
235
+ { listingId: listing.id, status: res.status },
236
+ "listing issue filing returned no html_url",
237
+ );
238
+ return;
239
+ }
240
+
241
+ await prisma.projectListing.update({
242
+ where: { id: listing.id },
243
+ data: { issueUrl: json.html_url },
244
+ });
245
+ } catch (err) {
246
+ logger.warn(
247
+ { listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
248
+ "listing issue filing threw",
249
+ );
250
+ }
251
+ }