create-ailk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -1,426 +0,0 @@
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
- }
@@ -1,251 +0,0 @@
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
- }