create-ailk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -0,0 +1,89 @@
1
+ /**
2
+ * #5334 — `loadSlice()` is the slice-module load boundary.
3
+ *
4
+ * The regression that matters most here is the LAST block: a slice that throws
5
+ * while evaluating must keep its own error object, type and stack. Wrapping it
6
+ * would turn a real bug in a slice into a misleading "could not be resolved"
7
+ * report, which is the opposite of what this boundary exists to do.
8
+ */
9
+ import { loadSlice } from "../slice-load.js";
10
+
11
+ function moduleNotFound(code: string, message: string): Error {
12
+ const error = new Error(message) as Error & { code: string };
13
+ error.code = code;
14
+ return error;
15
+ }
16
+
17
+ describe("loadSlice (#5334)", () => {
18
+ it("passes a resolving thunk's value straight through", async () => {
19
+ const mod = { workspaceContextRoutes: () => undefined };
20
+ await expect(loadSlice("./routes/workspaces/index.js", async () => mod))
21
+ .resolves.toBe(mod);
22
+ });
23
+
24
+ it("awaits the thunk rather than the specifier — the specifier is never imported", async () => {
25
+ const load = jest.fn(async () => ({ ok: true }));
26
+ await loadSlice("./never-imported.js", load);
27
+ expect(load).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ describe.each([
31
+ ["ERR_MODULE_NOT_FOUND", "the ESM resolver's code"],
32
+ ["MODULE_NOT_FOUND", "the CJS resolver's code"],
33
+ ])("on %s (%s)", (code) => {
34
+ const original = moduleNotFound(
35
+ code,
36
+ "Cannot find module '/x/apps/api/dist/list.js'",
37
+ );
38
+ const failing = async () => {
39
+ throw original;
40
+ };
41
+
42
+ it("names the specifier in the message", async () => {
43
+ await expect(loadSlice("./list.js", failing)).rejects.toThrow(
44
+ /AILK slice module '\.\/list\.js'/,
45
+ );
46
+ });
47
+
48
+ it("names BOTH causes, so a reader can tell a tracer miss from a bundled build", async () => {
49
+ const error = await loadSlice("./list.js", failing).catch(
50
+ (e: unknown) => e as Error,
51
+ );
52
+ expect(error.message).toMatch(/not SHIPPED/);
53
+ expect(error.message).toMatch(/slice-reachability:check/);
54
+ expect(error.message).toMatch(/BUNDLED/);
55
+ expect(error.message).toMatch(/apps\/api\/CLAUDE\.md/);
56
+ });
57
+
58
+ it("preserves the original error as `cause`", async () => {
59
+ const error = await loadSlice("./list.js", failing).catch(
60
+ (e: unknown) => e as Error,
61
+ );
62
+ expect(error.cause).toBe(original);
63
+ });
64
+ });
65
+
66
+ it("rethrows a non-resolution error IDENTICALLY — same object, not a wrapper", async () => {
67
+ const boom = new Error("slice blew up while evaluating");
68
+ const caught = await loadSlice("./list.js", async () => {
69
+ throw boom;
70
+ }).catch((e: unknown) => e);
71
+ expect(caught).toBe(boom);
72
+ expect((caught as Error).message).toBe("slice blew up while evaluating");
73
+ });
74
+
75
+ it("rethrows an errno-shaped error that is not a module-resolution code", async () => {
76
+ const denied = moduleNotFound("EACCES", "permission denied");
77
+ const caught = await loadSlice("./list.js", async () => {
78
+ throw denied;
79
+ }).catch((e: unknown) => e);
80
+ expect(caught).toBe(denied);
81
+ });
82
+
83
+ it("rethrows a non-Error throw untouched", async () => {
84
+ const caught = await loadSlice("./list.js", async () => {
85
+ throw "a string, because JavaScript";
86
+ }).catch((e: unknown) => e);
87
+ expect(caught).toBe("a string, because JavaScript");
88
+ });
89
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Slice-module load boundary (#5334 D4).
3
+ *
4
+ * Every paid-module SLICE in `apps/api` is reached through an indirect dynamic
5
+ * `import()` — a specifier the call site never writes as a literal, because
6
+ * `tsc` would then resolve it and the OSS strip would break (#5182 D1). #5304
7
+ * moved those specifiers onto module-level constants, the one shape that is
8
+ * simultaneously unresolvable to `tsc` and traceable to `@vercel/nft`, so the
9
+ * files reach a Vercel deployment.
10
+ *
11
+ * WHAT THIS FILE IS FOR. That shape is correct for the deploy path the scaffold
12
+ * ships, and it cannot be made to work under a BUNDLER. esbuild bundles a
13
+ * dynamic `import()` iff its argument is a string literal at the call site —
14
+ * exactly the predicate `tsc` resolves on, and exactly the one #5182 D1
15
+ * requires be false. So in a bundled build the specifier survives verbatim and
16
+ * Node resolves it against the BUNDLE OUTPUT directory rather than the source
17
+ * module's: `"./list.js"`, written in `routes/project-listings/index.ts`,
18
+ * becomes `dist/list.js` and does not exist. `build()` throws during
19
+ * CONSTRUCTION, so every route 500s rather than one.
20
+ *
21
+ * The failure is otherwise silent on every channel: esbuild emits no warning
22
+ * for the imports it declines to bundle, and a consuming frontend degrades to
23
+ * an empty list and still answers 200. `salessmyth/samhenry` spent nine hours
24
+ * on it. This boundary is what turns that into a one-line diagnosis — it does
25
+ * not make bundling work, because nothing in this file could.
26
+ *
27
+ * WHY IT TAKES A THUNK AND NOT A SPECIFIER. `loadSlice("./list.js")` doing the
28
+ * `import()` in here would be the obvious signature and would silently undo
29
+ * #5304. `enumerateSliceSpecifiers()` treats a file as an "indirect file" only
30
+ * if it carries a dynamic `import()` with a non-literal argument; move the call
31
+ * out of the callers and they stop qualifying, their specifier constants stop
32
+ * being candidates, and `check-slice-reachability.ts` passes vacuously over an
33
+ * empty set. Worse, `@vercel/nft` constant-propagates `WORKSPACES_INDEX` into
34
+ * `import(WORKSPACES_INDEX)` only because the constant and the call sit in one
35
+ * module — hoist the call and there is nothing to propagate, so every slice
36
+ * file goes untraced and #5304's outage returns with its own gate green.
37
+ *
38
+ * So the `import()` stays at the call site, in the caller's module, and this
39
+ * function only awaits the thunk and interprets the failure.
40
+ *
41
+ * Gated by `scripts/check-bundled-build-diagnoses.ts`, which bundles `apps/api`
42
+ * and asserts that the boot fails WITH this diagnostic rather than without one.
43
+ */
44
+
45
+ /**
46
+ * Node reports an unresolvable ESM specifier as `ERR_MODULE_NOT_FOUND` and an
47
+ * unresolvable CJS one as `MODULE_NOT_FOUND`. A bundle can produce either,
48
+ * since a bundled graph mixes both.
49
+ */
50
+ const MODULE_NOT_FOUND_CODES = new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]);
51
+
52
+ function isModuleNotFound(error: unknown): boolean {
53
+ const code = (error as { code?: unknown } | null | undefined)?.code;
54
+ return typeof code === "string" && MODULE_NOT_FOUND_CODES.has(code);
55
+ }
56
+
57
+ /**
58
+ * Await a slice module's loader thunk, turning an unresolvable module into a
59
+ * diagnosis instead of a bare `ERR_MODULE_NOT_FOUND` on a path that never
60
+ * existed.
61
+ *
62
+ * Anything that is not a module-resolution failure is rethrown UNTOUCHED — the
63
+ * same object, not a wrapper. A slice that throws while evaluating is a real
64
+ * bug in the slice and must keep its own stack and type.
65
+ *
66
+ * @param specifier the module specifier the thunk imports, for the message.
67
+ * Every call site already has it in scope, so nothing is enumerated here.
68
+ * @param load the caller's own `() => import(CONST)` thunk. Must close over a
69
+ * constant in the CALLER's module — see the file header.
70
+ */
71
+ export async function loadSlice(
72
+ specifier: string,
73
+ load: () => Promise<unknown>,
74
+ ): Promise<unknown> {
75
+ try {
76
+ return await load();
77
+ } catch (error) {
78
+ if (!isModuleNotFound(error)) throw error;
79
+ throw new Error(
80
+ `AILK slice module '${specifier}' could not be resolved at runtime. ` +
81
+ `This throws during server construction, so every route fails, not one. Two causes produce it:\n` +
82
+ ` 1. The module was compiled but not SHIPPED — the deploy-time file tracer did not emit it (#5304). ` +
83
+ `Check with \`pnpm slice-reachability:check\`.\n` +
84
+ ` 2. This build was BUNDLED. apps/api does not support bundling (#5334): a bundler resolves this ` +
85
+ `specifier against the bundle's own output directory rather than its source module's, and esbuild ` +
86
+ `leaves it unbundled without a warning. See apps/api/CLAUDE.md § Deploy path.`,
87
+ { cause: error },
88
+ );
89
+ }
90
+ }
@@ -28,10 +28,10 @@ describe("GET /api/openapi.json", () => {
28
28
 
29
29
  const spec = JSON.parse(res.body);
30
30
  expect(spec.openapi).toBe("3.1.0");
31
- expect(spec.info).toMatchObject({
32
- title: "AI Launch KitPublic Content API",
33
- version: "1.0.0",
34
- });
31
+ // The title is resolved from the serving site's own `_site.mdx` (#5307),
32
+ // so assert its SHAPE rather than a literal site name the same build
33
+ // serves a different title on every consuming site.
34
+ expect(spec.info.title).toMatch(/Public Content API$/);
35
35
  expect(spec.info.version).toBe("1.0.0");
36
36
  expect(typeof spec.paths).toBe("object");
37
37
  });
@@ -54,39 +54,59 @@ describe("GET /api/openapi.json", () => {
54
54
  );
55
55
  });
56
56
 
57
- it("documents the checkout sessions endpoint with operationId createCheckoutSession", async () => {
58
- const res = await app.inject({
59
- method: "GET",
60
- url: "/v1/openapi.json",
57
+ // The checkout path's OpenAPI documentation is module-conditional (commerce,
58
+ // D-47 / #5364) — mirroring the route's own registration gate in server.ts —
59
+ // so these two assertions need the module enabled, exactly as a real
60
+ // commerce-installed deployment would have it.
61
+ describe("checkout endpoint (commerce module enabled)", () => {
62
+ const PRIOR_ENABLED_MODULES = process.env.AILK_ENABLED_MODULES;
63
+
64
+ beforeEach(() => {
65
+ process.env.AILK_ENABLED_MODULES = "commerce";
61
66
  });
62
67
 
63
- const spec = JSON.parse(res.body);
64
- expect(spec.paths["/v1/checkout/sessions"].post.operationId).toBe(
65
- "createCheckoutSession",
66
- );
67
- });
68
+ afterEach(() => {
69
+ if (PRIOR_ENABLED_MODULES === undefined) {
70
+ delete process.env.AILK_ENABLED_MODULES;
71
+ } else {
72
+ process.env.AILK_ENABLED_MODULES = PRIOR_ENABLED_MODULES;
73
+ }
74
+ });
68
75
 
69
- it("documents 201, 400, 404, 500, 502 responses for the checkout endpoint", async () => {
70
- const res = await app.inject({
71
- method: "GET",
72
- url: "/v1/openapi.json",
76
+ it("documents the checkout sessions endpoint with operationId createCheckoutSession", async () => {
77
+ const res = await app.inject({
78
+ method: "GET",
79
+ url: "/v1/openapi.json",
80
+ });
81
+
82
+ const spec = JSON.parse(res.body);
83
+ expect(spec.paths["/v1/checkout/sessions"].post.operationId).toBe(
84
+ "createCheckoutSession",
85
+ );
73
86
  });
74
87
 
75
- const spec = JSON.parse(res.body);
76
- const responses = spec.paths["/v1/checkout/sessions"].post.responses;
77
- expect(responses["201"].description).toBe("Checkout session created");
78
- expect(responses["400"].description).toBe(
79
- "Validation error — invalid request body",
80
- );
81
- expect(responses["404"].description).toBe(
82
- "Unknown product code — no active Stripe price for the given productCode",
83
- );
84
- expect(responses["500"].description).toBe(
85
- "Live-mode forbiddenSTRIPE_SECRET_KEY is a live key",
86
- );
87
- expect(responses["502"].description).toBe(
88
- "Checkout unavailable — Stripe returned an unexpected error",
89
- );
88
+ it("documents 201, 400, 404, 500, 502 responses for the checkout endpoint", async () => {
89
+ const res = await app.inject({
90
+ method: "GET",
91
+ url: "/v1/openapi.json",
92
+ });
93
+
94
+ const spec = JSON.parse(res.body);
95
+ const responses = spec.paths["/v1/checkout/sessions"].post.responses;
96
+ expect(responses["201"].description).toBe("Checkout session created");
97
+ expect(responses["400"].description).toBe(
98
+ "Validation errorinvalid request body",
99
+ );
100
+ expect(responses["404"].description).toBe(
101
+ "Unknown product code no active Stripe price for the given productCode",
102
+ );
103
+ expect(responses["500"].description).toBe(
104
+ "Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
105
+ );
106
+ expect(responses["502"].description).toBe(
107
+ "Checkout unavailable — Stripe returned an unexpected error",
108
+ );
109
+ });
90
110
  });
91
111
 
92
112
  it("declares query and path parameters for content endpoints", async () => {
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file spec-identity.test.ts
3
+ *
4
+ * The served API's self-description (#5307) and the `servers` block (#5306).
5
+ *
6
+ * These assert the BUILDER's output given a config rather than a deployed
7
+ * response: the defect is that a deployed site serves the kit's identity and
8
+ * the kit's host, and the builder is the only place that can be proved
9
+ * without deploying.
10
+ */
11
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import {
16
+ API_VERSION,
17
+ apiDescription,
18
+ apiDisplayName,
19
+ buildOpenApiSpec,
20
+ resetApiIdentity,
21
+ resolveApiIdentity,
22
+ } from "../spec";
23
+
24
+ /** Write a throwaway content root carrying the given `_site.mdx` frontmatter. */
25
+ function withContentRoot(frontmatter: string): string {
26
+ const root = mkdtempSync(join(tmpdir(), "ailk-site-"));
27
+ mkdirSync(root, { recursive: true });
28
+ writeFileSync(join(root, "_site.mdx"), `---\n${frontmatter}\n---\n\nbody\n`);
29
+ return root;
30
+ }
31
+
32
+ describe("buildOpenApiSpec — site identity (#5307)", () => {
33
+ it("titles the spec after the site, not after the kit", () => {
34
+ const spec = buildOpenApiSpec({ siteName: "Sam Henry" });
35
+
36
+ expect(spec.info.title).toBe("Sam Henry — Public Content API");
37
+ expect(JSON.stringify(spec.info)).not.toMatch(/AI Launch Kit|AILK/i);
38
+ });
39
+
40
+ it("degrades to a named neutral title when no site name is resolvable", () => {
41
+ const spec = buildOpenApiSpec({});
42
+
43
+ expect(spec.info.title).toBe("Public Content API");
44
+ expect(spec.info.title).not.toMatch(/AI Launch Kit|AILK/i);
45
+ });
46
+
47
+ it("asserts no licence over a deployment that has not chosen one", () => {
48
+ expect(buildOpenApiSpec({ siteName: "Sam Henry" }).info).not.toHaveProperty(
49
+ "license",
50
+ );
51
+ });
52
+
53
+ it("keeps API_VERSION as the /v1 contract version", () => {
54
+ expect(API_VERSION).toBe("1.0.0");
55
+ expect(buildOpenApiSpec({}).info.version).toBe(API_VERSION);
56
+ });
57
+ });
58
+
59
+ describe("buildOpenApiSpec — servers block (#5306)", () => {
60
+ it("names the deployment's own API origin", () => {
61
+ const spec = buildOpenApiSpec({ publicApiUrl: "https://api.samhenry.org" });
62
+
63
+ expect(spec.servers).toEqual([
64
+ { url: "https://api.samhenry.org", description: expect.any(String) },
65
+ ]);
66
+ });
67
+
68
+ it("never serves the kit's own host", () => {
69
+ const spec = buildOpenApiSpec({
70
+ siteName: "Sam Henry",
71
+ publicApiUrl: "https://api.samhenry.org",
72
+ });
73
+
74
+ expect(JSON.stringify(spec)).not.toContain("ailaunchkit.ai");
75
+ });
76
+
77
+ it("normalises a trailing slash", () => {
78
+ expect(
79
+ buildOpenApiSpec({ publicApiUrl: "https://api.samhenry.org/" }).servers[0]
80
+ ?.url,
81
+ ).toBe("https://api.samhenry.org");
82
+ });
83
+
84
+ it("degrades to a document-relative origin when the value is unset", () => {
85
+ const spec = buildOpenApiSpec({});
86
+
87
+ expect(spec.servers).toHaveLength(1);
88
+ expect(spec.servers[0]?.url).toBe("/");
89
+ // The degradation is named in the served document itself, so an agent
90
+ // reading the spec learns why rather than following a wrong absolute URL.
91
+ expect(spec.servers[0]?.description).toMatch(/AILK_PUBLIC_API_URL/);
92
+ });
93
+
94
+ it("degrades rather than serving a malformed origin", () => {
95
+ expect(buildOpenApiSpec({ publicApiUrl: "not-a-url" }).servers[0]?.url).toBe(
96
+ "/",
97
+ );
98
+ expect(
99
+ buildOpenApiSpec({ publicApiUrl: "ftp://api.example.com" }).servers[0]
100
+ ?.url,
101
+ ).toBe("/");
102
+ });
103
+ });
104
+
105
+ describe("resolveApiIdentity — reads _site.mdx", () => {
106
+ const originalContentRoot = process.env.CONTENT_ROOT;
107
+ let root: string | undefined;
108
+
109
+ afterEach(() => {
110
+ if (originalContentRoot === undefined) delete process.env.CONTENT_ROOT;
111
+ else process.env.CONTENT_ROOT = originalContentRoot;
112
+ if (root) rmSync(root, { recursive: true, force: true });
113
+ root = undefined;
114
+ resetApiIdentity();
115
+ });
116
+
117
+ it("sources the site name and description from _site.mdx", () => {
118
+ root = withContentRoot(
119
+ [
120
+ 'version: "1"',
121
+ "siteName: Sam Henry",
122
+ "baseUrl: https://samhenry.org",
123
+ "siteIdentity:",
124
+ " kind: Person",
125
+ " name: Sam Henry",
126
+ " description: Independent software consultant.",
127
+ ].join("\n"),
128
+ );
129
+ process.env.CONTENT_ROOT = root;
130
+ resetApiIdentity();
131
+
132
+ const identity = resolveApiIdentity();
133
+
134
+ expect(identity.siteName).toBe("Sam Henry");
135
+ expect(identity.siteDescription).toBe("Independent software consultant.");
136
+ expect(apiDisplayName(identity)).toBe("Sam Henry API");
137
+ expect(apiDescription(identity)).toBe("Independent software consultant.");
138
+ });
139
+
140
+ // #5306 AC-3 — the trap this card exists to refuse.
141
+ it("does NOT use _site.mdx baseUrl as the API origin", () => {
142
+ root = withContentRoot(
143
+ [
144
+ 'version: "1"',
145
+ "siteName: Sam Henry",
146
+ // The SITE origin. The API is deployed on a different host
147
+ // (api.samhenry.org), so this value must never reach `servers`.
148
+ "baseUrl: https://samhenry.org",
149
+ ].join("\n"),
150
+ );
151
+ process.env.CONTENT_ROOT = root;
152
+ resetApiIdentity();
153
+
154
+ const identity = resolveApiIdentity();
155
+ expect(identity.publicApiUrl).not.toBe("https://samhenry.org");
156
+
157
+ const spec = buildOpenApiSpec({ ...identity, publicApiUrl: undefined });
158
+ expect(JSON.stringify(spec.servers)).not.toContain("samhenry.org");
159
+ });
160
+
161
+ it("degrades to a named neutral default when _site.mdx is unreadable", () => {
162
+ root = mkdtempSync(join(tmpdir(), "ailk-empty-"));
163
+ process.env.CONTENT_ROOT = root;
164
+ resetApiIdentity();
165
+
166
+ const identity = resolveApiIdentity();
167
+
168
+ expect(identity.siteName).toBeUndefined();
169
+ expect(apiDisplayName(identity)).toBe("Content API");
170
+ expect(apiDisplayName(identity)).not.toMatch(/AI Launch Kit|AILK/i);
171
+ expect(buildOpenApiSpec(identity).info.title).toBe("Public Content API");
172
+ });
173
+
174
+ it("degrades when _site.mdx frontmatter is malformed", () => {
175
+ root = withContentRoot("siteName: [unclosed");
176
+ process.env.CONTENT_ROOT = root;
177
+ resetApiIdentity();
178
+
179
+ expect(resolveApiIdentity().siteName).toBeUndefined();
180
+ expect(apiDisplayName(resolveApiIdentity())).toBe("Content API");
181
+ });
182
+ });