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
@@ -32,6 +32,10 @@
32
32
  import type { VercelRequest, VercelResponse } from '@vercel/node';
33
33
  import Fastify from 'fastify';
34
34
 
35
+ jest.mock('@working-theory/observability', () => ({
36
+ logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() },
37
+ }));
38
+
35
39
  // ── Typed request/response stubs ────────────────────────────────────────────
36
40
  //
37
41
  // Minimal stubs that satisfy only the VercelRequest/VercelResponse surface
@@ -209,3 +213,158 @@ describe('vercel-handler — response side: binary round-trip (#5168)', () => {
209
213
  expect(Buffer.compare(res._sent as Buffer, rawPayload)).toBe(0);
210
214
  });
211
215
  });
216
+
217
+ // ── #5332: one build() per process ──────────────────────────────────────────
218
+ //
219
+ // getServer() used to cache the RESOLVED instance and test it before two
220
+ // awaits, so two invocations overlapping a cold start on one warm container
221
+ // both saw null and both built a complete Fastify server. The fix caches the
222
+ // in-flight promise instead. These tests drive getServer() through the default
223
+ // handler — the handler's exported shape is byte-synced into consuming sites,
224
+ // so getServer() stays module-private rather than being exported for the test.
225
+
226
+ const COLD_START_REQUEST: MockVercelRequest = {
227
+ method: 'GET',
228
+ url: '/health',
229
+ headers: {},
230
+ body: undefined,
231
+ query: {},
232
+ };
233
+
234
+ function sentMessage(_res: MockRes): string {
235
+ // Re-required, not imported: jest.resetModules() gives each test a fresh
236
+ // mock instance, and the handler under test holds that fresh one.
237
+ const { logger } = require('@working-theory/observability') as {
238
+ logger: { error: jest.Mock };
239
+ };
240
+ const calls = logger.error.mock.calls.filter(
241
+ ([, msg]) => msg === '[Vercel Handler] Error handling request',
242
+ );
243
+ const last = calls[calls.length - 1];
244
+ return last ? String((last[0] as { err?: Error }).err?.message ?? '') : '';
245
+ }
246
+
247
+ describe('vercel-handler — one build per process (#5332)', () => {
248
+ beforeEach(() => {
249
+ jest.resetModules();
250
+ });
251
+
252
+ afterEach(() => {
253
+ jest.useRealTimers();
254
+ });
255
+
256
+ it('builds one server for two invocations that overlap a cold start', async () => {
257
+ // One shared deferred for both calls, so whichever build() calls happen
258
+ // all settle together — the assertion is on the CALL COUNT, and a test
259
+ // that hung when the second call won the cache would obscure it.
260
+ let resolveServer!: (instance: unknown) => void;
261
+ const serverReady = new Promise<unknown>((resolve) => {
262
+ resolveServer = resolve;
263
+ });
264
+ const inject = jest.fn().mockResolvedValue({
265
+ statusCode: 200,
266
+ headers: {},
267
+ body: 'ok',
268
+ rawPayload: Buffer.from('ok'),
269
+ });
270
+ const build = jest.fn(() => serverReady);
271
+ jest.doMock('../server', () => ({ build }));
272
+ const handler = loadHandler();
273
+
274
+ const resA = makeRes();
275
+ const resB = makeRes();
276
+ // Both started before either resolves: neither has a server yet.
277
+ const a = handler(asRequest(COLD_START_REQUEST), asResponse(resA));
278
+ const b = handler(asRequest(COLD_START_REQUEST), asResponse(resB));
279
+
280
+ // Drain the microtask queue so both calls have reached getServer() and
281
+ // awaited build() before the build is allowed to finish.
282
+ for (let i = 0; i < 10; i += 1) {
283
+ await new Promise((resolve) => setImmediate(resolve));
284
+ }
285
+
286
+ resolveServer({ inject });
287
+ await Promise.all([a, b]);
288
+
289
+ expect(build).toHaveBeenCalledTimes(1);
290
+ expect(resA._status).toBe(200);
291
+ expect(resB._status).toBe(200);
292
+ });
293
+
294
+ it('retries the build on the next invocation after a failed cold start', async () => {
295
+ const inject = jest.fn().mockResolvedValue({
296
+ statusCode: 200,
297
+ headers: {},
298
+ body: 'ok',
299
+ rawPayload: Buffer.from('ok'),
300
+ });
301
+ const build = jest
302
+ .fn()
303
+ .mockRejectedValueOnce(new Error('prisma connect refused'))
304
+ .mockResolvedValue({ inject });
305
+ jest.doMock('../server', () => ({ build }));
306
+ const handler = loadHandler();
307
+
308
+ const failed = makeRes();
309
+ await handler(asRequest(COLD_START_REQUEST), asResponse(failed));
310
+ expect(failed._status).toBe(500);
311
+ expect(sentMessage(failed)).toBe('Server build failed: prisma connect refused');
312
+
313
+ // A rejected promise left in the cache would serve this request the same
314
+ // failure forever without calling build() again.
315
+ const retried = makeRes();
316
+ await handler(asRequest(COLD_START_REQUEST), asResponse(retried));
317
+
318
+ expect(build).toHaveBeenCalledTimes(2);
319
+ expect(retried._status).toBe(200);
320
+ });
321
+
322
+ it('does not leak an absolute filesystem path in the 500 body (#5351)', async () => {
323
+ const leakyPath = '/home/deploy/apps/api/dist/list.js';
324
+ const build = jest.fn().mockRejectedValue(
325
+ new Error(`ERR_MODULE_NOT_FOUND file://${leakyPath}`)
326
+ );
327
+ jest.doMock('../server', () => ({ build }));
328
+ const handler = loadHandler();
329
+
330
+ const res = makeRes();
331
+ await handler(asRequest(COLD_START_REQUEST), asResponse(res));
332
+
333
+ expect(res._status).toBe(500);
334
+ expect(JSON.stringify(res._sent)).not.toContain(leakyPath);
335
+ });
336
+
337
+ it('still fires the build timeout with its original message', async () => {
338
+ jest.useFakeTimers();
339
+ // A module object carrying its own `then` is assimilated when the
340
+ // handler's `await import('./server.js')` resolves with it, so the
341
+ // dynamic import never settles — the production condition the 10s build
342
+ // timeout guards. `__esModule` keeps ts-jest's __importStar from copying
343
+ // the object and dropping `then`.
344
+ jest.doMock('../server', () => ({ __esModule: true, then: () => {} }));
345
+ const handler = loadHandler();
346
+
347
+ const res = makeRes();
348
+ const pending = handler(asRequest(COLD_START_REQUEST), asResponse(res));
349
+ await jest.advanceTimersByTimeAsync(10_000);
350
+ await pending;
351
+
352
+ expect(res._status).toBe(500);
353
+ expect(sentMessage(res)).toBe('Server build failed: Server build timeout after 10s');
354
+ });
355
+
356
+ it('still fires the init timeout with its original message', async () => {
357
+ jest.useFakeTimers();
358
+ const build = jest.fn(() => new Promise(() => {}));
359
+ jest.doMock('../server', () => ({ build }));
360
+ const handler = loadHandler();
361
+
362
+ const res = makeRes();
363
+ const pending = handler(asRequest(COLD_START_REQUEST), asResponse(res));
364
+ await jest.advanceTimersByTimeAsync(10_000);
365
+ await pending;
366
+
367
+ expect(res._status).toBe(500);
368
+ expect(sentMessage(res)).toBe('Server build failed: Server initialization timeout after 10s');
369
+ });
370
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @file workspace-module-registration.test.ts
3
+ * @description The `workspace || multi-tenant` registration matrix (#5371 D2).
4
+ *
5
+ * `workspace` cannot be folded into the sibling `module-exclusion.test.ts`:
6
+ * that suite's shape is "excluding module X 404s every route X owns", and the
7
+ * three routes here register under EITHER module, so excluding `workspace`
8
+ * while `multi-tenant` is on leaves them registered by design. That is the
9
+ * whole point of D2's gate, so it gets its own matrix.
10
+ *
11
+ * The three cases §5.1 names:
12
+ *
13
+ * 1. workspace only — the three workspace-settings routes resolve for a
14
+ * single-tenant session; billing / workspace-session
15
+ * / org routes stay unregistered.
16
+ * 2. multi-tenant only — everything resolves exactly as it did before this
17
+ * change (the regression floor).
18
+ * 3. both enabled — each of workspaceConfigRoutes /
19
+ * workspaceGeneralRoutes / workspaceMembersRoutes
20
+ * registers exactly ONCE; Fastify throws on a
21
+ * duplicate route at build() time, so a successful
22
+ * build IS the assertion, backed by an explicit
23
+ * count over the router tree.
24
+ *
25
+ * "Resolves" is measured as "not Fastify's own 404". An unauthenticated
26
+ * request to a registered tenant route answers 401, never 404, so the two
27
+ * states are unambiguous — the same signal `module-exclusion.test.ts` uses.
28
+ *
29
+ * Mirrors `src/__tests__/server.test.ts`'s external-boundary mocks (CORS,
30
+ * rate-limit, email) — see that file's header for why each is mocked.
31
+ */
32
+
33
+ jest.mock('@fastify/cors', () => {
34
+ return jest.fn().mockResolvedValue(undefined);
35
+ });
36
+
37
+ jest.mock('@fastify/rate-limit', () => {
38
+ return jest.fn().mockResolvedValue(undefined);
39
+ });
40
+
41
+ jest.mock('@working-theory/email', () => ({
42
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
43
+ }));
44
+
45
+ import { build } from '../server';
46
+
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
48
+ type App = any;
49
+
50
+ const WS = 'wksp_00000000-0000-7000-8000-0000000000aa';
51
+
52
+ /** The routes the widened gate must serve under EITHER module. */
53
+ const SHARED_TENANT_ROUTES = [
54
+ { method: 'GET' as const, url: `/v1/workspaces/${WS}/theme` },
55
+ { method: 'GET' as const, url: `/v1/workspaces/${WS}/identity` },
56
+ { method: 'GET' as const, url: `/v1/workspaces/${WS}/seo` },
57
+ { method: 'GET' as const, url: `/v1/workspaces/${WS}/general` },
58
+ { method: 'GET' as const, url: `/v1/workspaces/${WS}/members` },
59
+ ];
60
+
61
+ /** Routes that must stay multi-tenant-only — the gate's other half. */
62
+ const MULTI_TENANT_ONLY_ROUTES = [
63
+ { method: 'GET' as const, url: '/v1/workspaces' },
64
+ { method: 'GET' as const, url: '/v1/billing' },
65
+ { method: 'GET' as const, url: '/v1/waitlist-experiments' },
66
+ ];
67
+
68
+ async function withEnabledModules<T>(
69
+ enabled: string,
70
+ fn: () => Promise<T>,
71
+ ): Promise<T> {
72
+ const prior = process.env.AILK_ENABLED_MODULES;
73
+ process.env.AILK_ENABLED_MODULES = enabled;
74
+ try {
75
+ return await fn();
76
+ } finally {
77
+ if (prior === undefined) {
78
+ delete process.env.AILK_ENABLED_MODULES;
79
+ } else {
80
+ process.env.AILK_ENABLED_MODULES = prior;
81
+ }
82
+ }
83
+ }
84
+
85
+ /** Every (method, path) pair Fastify's router holds, one per line. */
86
+ function routeLines(app: App): string[] {
87
+ return (app.printRoutes({ commonPrefix: false }) as string)
88
+ .split('\n')
89
+ .map((line: string) => line.trim())
90
+ .filter(Boolean);
91
+ }
92
+
93
+ describe('workspace-settings registration matrix (#5371 D2)', () => {
94
+ let app: App;
95
+
96
+ afterEach(async () => {
97
+ if (app) {
98
+ await app.close();
99
+ app = undefined;
100
+ }
101
+ delete process.env.AILK_ENABLED_MODULES;
102
+ });
103
+
104
+ it("with 'workspace' enabled and 'multi-tenant' disabled, the five settings routes resolve", async () => {
105
+ app = await withEnabledModules('workspace', () => build());
106
+
107
+ for (const { method, url } of SHARED_TENANT_ROUTES) {
108
+ const res = await app.inject({ method, url });
109
+ expect({ url, status: res.statusCode }).toEqual({ url, status: 401 });
110
+ }
111
+ });
112
+
113
+ it("with 'workspace' alone, billing / workspace-session / org routes stay unregistered", async () => {
114
+ app = await withEnabledModules('workspace', () => build());
115
+
116
+ for (const { method, url } of MULTI_TENANT_ONLY_ROUTES) {
117
+ const res = await app.inject({ method, url });
118
+ expect({ url, status: res.statusCode }).toEqual({ url, status: 404 });
119
+ }
120
+ });
121
+
122
+ it("with 'multi-tenant' enabled and 'workspace' disabled, every route resolves as before", async () => {
123
+ app = await withEnabledModules('multi-tenant', () => build());
124
+
125
+ for (const { method, url } of [
126
+ ...SHARED_TENANT_ROUTES,
127
+ ...MULTI_TENANT_ONLY_ROUTES,
128
+ ]) {
129
+ const res = await app.inject({ method, url });
130
+ expect({ url, status: res.statusCode }).not.toEqual({ url, status: 404 });
131
+ }
132
+ });
133
+
134
+ it('with BOTH enabled, each shared route registers exactly once', async () => {
135
+ // build() itself is the first assertion: Fastify throws
136
+ // FST_ERR_DUPLICATED_ROUTE at registration time, so a double-registration
137
+ // never reaches the router tree to be counted.
138
+ app = await withEnabledModules('workspace,multi-tenant', () => build());
139
+
140
+ const lines = routeLines(app);
141
+ for (const segment of ['theme', 'identity', 'seo', 'general', 'members']) {
142
+ const matches = lines.filter((line) => line.includes(segment));
143
+ expect({ segment, count: matches.length }).toEqual({
144
+ segment,
145
+ count: 1,
146
+ });
147
+ }
148
+
149
+ for (const { method, url } of [
150
+ ...SHARED_TENANT_ROUTES,
151
+ ...MULTI_TENANT_ONLY_ROUTES,
152
+ ]) {
153
+ const res = await app.inject({ method, url });
154
+ expect({ url, status: res.statusCode }).not.toEqual({ url, status: 404 });
155
+ }
156
+ });
157
+
158
+ it('with neither enabled, none of the five settings routes registers', async () => {
159
+ app = await withEnabledModules('', () => build());
160
+
161
+ for (const { method, url } of SHARED_TENANT_ROUTES) {
162
+ const res = await app.inject({ method, url });
163
+ expect({ url, status: res.statusCode }).toEqual({ url, status: 404 });
164
+ }
165
+ });
166
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `isModuleEnabled()` default-flip regression test (D-44, #5368).
3
+ *
4
+ * Proves the mechanical claim #5368 exists to make true: an UNSET
5
+ * `AILK_ENABLED_MODULES` now gates every paid module OFF, the same as an
6
+ * explicit empty string — only an explicit allowlist turns modules on, and
7
+ * turns on exactly the modules it names.
8
+ *
9
+ * Reverting `enabledModules()`'s unset branch back to the `undefined`
10
+ * sentinel (S1, #5088's original shape) makes the "unset" cases below fail
11
+ * RED — see #5368 verification notes.
12
+ */
13
+
14
+ import { isModuleEnabled } from '../modules';
15
+
16
+ const RECOGNIZED_MODULES = [
17
+ 'docs',
18
+ 'multi-tenant',
19
+ 'scheduling',
20
+ 'content-write',
21
+ ] as const;
22
+
23
+ describe('isModuleEnabled', () => {
24
+ const originalEnv = process.env.AILK_ENABLED_MODULES;
25
+
26
+ afterEach(() => {
27
+ if (originalEnv === undefined) {
28
+ delete process.env.AILK_ENABLED_MODULES;
29
+ } else {
30
+ process.env.AILK_ENABLED_MODULES = originalEnv;
31
+ }
32
+ });
33
+
34
+ describe('AILK_ENABLED_MODULES unset', () => {
35
+ it.each(RECOGNIZED_MODULES)('returns false for %s', (moduleName) => {
36
+ delete process.env.AILK_ENABLED_MODULES;
37
+ expect(isModuleEnabled(moduleName)).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe('AILK_ENABLED_MODULES=""', () => {
42
+ it.each(RECOGNIZED_MODULES)('returns false for %s', (moduleName) => {
43
+ process.env.AILK_ENABLED_MODULES = '';
44
+ expect(isModuleEnabled(moduleName)).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe('AILK_ENABLED_MODULES as an explicit allowlist', () => {
49
+ it.each(RECOGNIZED_MODULES)(
50
+ 'enables exactly the listed modules — %s on when named',
51
+ (moduleName) => {
52
+ process.env.AILK_ENABLED_MODULES = moduleName;
53
+ expect(isModuleEnabled(moduleName)).toBe(true);
54
+ for (const other of RECOGNIZED_MODULES) {
55
+ if (other === moduleName) continue;
56
+ expect(isModuleEnabled(other)).toBe(false);
57
+ }
58
+ },
59
+ );
60
+
61
+ it('enables every module named in a comma list, and no others', () => {
62
+ process.env.AILK_ENABLED_MODULES = 'multi-tenant,scheduling';
63
+ expect(isModuleEnabled('multi-tenant')).toBe(true);
64
+ expect(isModuleEnabled('scheduling')).toBe(true);
65
+ expect(isModuleEnabled('docs')).toBe(false);
66
+ expect(isModuleEnabled('content-write')).toBe(false);
67
+ });
68
+
69
+ it('trims whitespace and drops empty entries, same as before the flip', () => {
70
+ process.env.AILK_ENABLED_MODULES = ' multi-tenant , , scheduling ';
71
+ expect(isModuleEnabled('multi-tenant')).toBe(true);
72
+ expect(isModuleEnabled('scheduling')).toBe(true);
73
+ expect(isModuleEnabled('docs')).toBe(false);
74
+ });
75
+ });
76
+ });
@@ -39,6 +39,21 @@ export interface Config {
39
39
  // row's issueUrl stays null for a later retry via the PATCH route.
40
40
  AILK_GITHUB_REPO?: string;
41
41
  AILK_GITHUB_TOKEN?: string;
42
+
43
+ // The public origin this API is reachable at, e.g. https://api.example.com.
44
+ // Already consumed by the deliverable download link (#3961 D5); the OpenAPI
45
+ // `servers` block reads it too (#5306), since the spec is byte-synced into
46
+ // every consuming site and cannot name a host as a literal.
47
+ //
48
+ // This is the API's OWN origin, distinct from FRONTEND_URL (the site the
49
+ // browser loads) and from `_site.mdx`'s `baseUrl` (the site's public web
50
+ // origin). A deployment typically serves the site from example.com and this
51
+ // API from api.example.com, so neither of the other two can stand in.
52
+ //
53
+ // Optional: unset degrades the `servers` block to a document-relative
54
+ // origin, which stays correct for any agent that fetched the spec from this
55
+ // API. See resolveServers() in src/openapi/spec.ts.
56
+ AILK_PUBLIC_API_URL?: string;
42
57
  }
43
58
 
44
59
  function validateConfig(): Config {
@@ -85,6 +100,10 @@ function validateConfig(): Config {
85
100
  ...(process.env.AILK_GITHUB_TOKEN !== undefined && {
86
101
  AILK_GITHUB_TOKEN: process.env.AILK_GITHUB_TOKEN,
87
102
  }),
103
+
104
+ ...(process.env.AILK_PUBLIC_API_URL !== undefined && {
105
+ AILK_PUBLIC_API_URL: process.env.AILK_PUBLIC_API_URL,
106
+ }),
88
107
  };
89
108
  }
90
109
 
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Paid-module presence — the registration-grain signal used by `server.ts`
3
3
  * to decide whether a paid module's routes (and their imports) are wired
4
- * into this build (S1, #5088).
4
+ * into this build (S1, #5088; defaulted off by D-44, #5368).
5
5
  *
6
6
  * `AILK_ENABLED_MODULES` is a comma-separated allowlist (e.g.
7
- * `"multi-tenant,scheduling"`). Unset means "every module on" — today's
8
- * behavior, unchanged for every existing deployment; an explicit empty
9
- * string (`""`) means "no paid modules" (the OSS-strip build target). This
10
- * mirrors `shouldMountTestAuth()`'s fail-open-by-default / explicit-opt-in
11
- * shape elsewhere in this file's sibling config.
7
+ * `"multi-tenant,scheduling"`). Unset means **no paid modules** — installed
8
+ * modules enable themselves explicitly; an explicit empty string (`""`)
9
+ * means the same (the OSS-strip build target); a list enables exactly the
10
+ * modules it names.
12
11
  *
13
- * Recognized module keys: `docs`, `multi-tenant`, `scheduling` (the
14
- * `ModuleName` vocabulary `packages/create-ailk/src/module-architecture.ts`
15
- * already models via `MODULE_SLICES`) plus `content-write` — the Pro
12
+ * Recognized module keys: `docs`, `multi-tenant`, `workspace`, `scheduling`,
13
+ * `signal-collectors`, `commerce` (the `ModuleName` vocabulary
14
+ * `packages/create-ailk/src/module-architecture.ts` already models via
15
+ * `MODULE_SLICES`) plus `content-write` — the Pro
16
16
  * content-write surface (`create`/`update`/`delete` under
17
17
  * `apps/api/src/routes/content/`). `content-write` is intentionally NOT a
18
18
  * `MODULE_SLICES` entry: it is not (yet) a `create-ailk --modules`
@@ -24,9 +24,9 @@
24
24
  * create-ailk's module types (outside its package allowlist anyway).
25
25
  */
26
26
 
27
- function enabledModules(): Set<string> | undefined {
27
+ function enabledModules(): Set<string> {
28
28
  const raw = process.env.AILK_ENABLED_MODULES;
29
- if (raw === undefined) return undefined; // undefined sentinel = "all enabled"
29
+ if (raw === undefined) return new Set(); // unset = no paid modules
30
30
  return new Set(
31
31
  raw
32
32
  .split(',')
@@ -37,7 +37,5 @@ function enabledModules(): Set<string> | undefined {
37
37
 
38
38
  /** Whether the named paid module's routes should register on this build/boot. */
39
39
  export function isModuleEnabled(moduleName: string): boolean {
40
- const enabled = enabledModules();
41
- if (enabled === undefined) return true;
42
- return enabled.has(moduleName);
40
+ return enabledModules().has(moduleName);
43
41
  }
@@ -194,6 +194,9 @@ export const prisma = {
194
194
  findMany: jest.fn(),
195
195
  update: jest.fn(),
196
196
  updateMany: jest.fn(),
197
+ // `count` — the resume email's per-recipient send cap (#5406): how many
198
+ // drafts this address has started on this site inside the rolling window.
199
+ count: jest.fn(),
197
200
  },
198
201
  // ── Inbox namespace (WaitlistExperiment — #4661 D5) ──────────────────────
199
202
  // `upsert` — the ONLY write path into waitlist_experiments
@@ -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
+ });