create-ailk 0.1.1 → 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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  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 +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @file surface-drift.test.ts
3
+ * @description The self-description drift gate (#5314).
4
+ *
5
+ * Builds the REAL server, takes the route table Fastify actually registered,
6
+ * and diffs it against both surfaces that claim to describe it — the root
7
+ * index `endpoints` object and the served OpenAPI `paths` object.
8
+ *
9
+ * Both surfaces are read over `inject`, i.e. exactly what an agent would
10
+ * fetch, never the object the builder returned.
11
+ *
12
+ * This gate was RED before the derivation landed: 61 registered routes were
13
+ * missing from `paths` and 53 from `endpoints`. Two of its five concerns
14
+ * (AC-1 coverage on each surface) are unfailable now that both surfaces
15
+ * derive from the same registry they are compared against — that is the
16
+ * inherent consequence of AC-2's "derived" branch, and the spec's D5 says so.
17
+ * What stays live is the overlay join (a hand-authored entry whose route no
18
+ * longer registers — the concern that caught `DELETE /v1/auth/tokens`, whose
19
+ * real path is `/v1/auth/tokens/:id`), the anti-vacuity floor (an empty or
20
+ * stale registry would pass a coverage check trivially, so it is asserted
21
+ * against directly), and the module-agreement case's non-empty guard, which
22
+ * fails if module gating ever collapses so both configs register identically.
23
+ *
24
+ * Mirrors `src/__tests__/module-exclusion.test.ts`'s external-boundary mocks
25
+ * (CORS, rate-limit, email) — see that file's header for why each is mocked.
26
+ */
27
+
28
+ jest.mock("@fastify/cors", () => {
29
+ return jest.fn().mockResolvedValue(undefined);
30
+ });
31
+
32
+ jest.mock("@fastify/rate-limit", () => {
33
+ return jest.fn().mockResolvedValue(undefined);
34
+ });
35
+
36
+ jest.mock("@working-theory/email", () => ({
37
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
38
+ }));
39
+
40
+ import { build } from "../../server";
41
+ import { readRegisteredRoutes } from "../spec";
42
+ import {
43
+ compareSurfaces,
44
+ formatSurfaceDrift,
45
+ hasDrift,
46
+ openApiSurfaceKeys,
47
+ rootIndexSurfaceKeys,
48
+ routeKey,
49
+ toOpenApiPath,
50
+ type RegisteredRoute,
51
+ } from "../surface-drift";
52
+
53
+ /** A route every build registers, outside every module conditional. */
54
+ const FLOOR_ROUTE = routeKey("GET", "/v1/content/types");
55
+
56
+ interface Surfaces {
57
+ registered: RegisteredRoute[];
58
+ paths: Record<string, Record<string, unknown>>;
59
+ endpoints: Record<string, string>;
60
+ }
61
+
62
+ /**
63
+ * Build the server under one module configuration and read both surfaces back
64
+ * over the wire. `undefined` means "leave AILK_ENABLED_MODULES unset", which
65
+ * is now the default deployment shape (no paid module, D-44 / #5368).
66
+ */
67
+ async function surfacesFor(enabled: string | undefined): Promise<Surfaces> {
68
+ const prior = process.env.AILK_ENABLED_MODULES;
69
+ if (enabled === undefined) {
70
+ delete process.env.AILK_ENABLED_MODULES;
71
+ } else {
72
+ process.env.AILK_ENABLED_MODULES = enabled;
73
+ }
74
+
75
+ try {
76
+ const app = await build();
77
+ await app.ready();
78
+
79
+ const registered = [...readRegisteredRoutes(app)];
80
+
81
+ const rootRes = await app.inject({ method: "GET", url: "/" });
82
+ const specRes = await app.inject({
83
+ method: "GET",
84
+ url: "/v1/openapi.json",
85
+ });
86
+
87
+ await app.close();
88
+
89
+ expect(rootRes.statusCode).toBe(200);
90
+ expect(specRes.statusCode).toBe(200);
91
+
92
+ return {
93
+ registered,
94
+ paths: JSON.parse(specRes.body).paths,
95
+ endpoints: JSON.parse(rootRes.body).endpoints,
96
+ };
97
+ } finally {
98
+ if (prior === undefined) {
99
+ delete process.env.AILK_ENABLED_MODULES;
100
+ } else {
101
+ process.env.AILK_ENABLED_MODULES = prior;
102
+ }
103
+ }
104
+ }
105
+
106
+ // The module configurations every claim below is asserted under. Unset is now
107
+ // the default deployment (no paid module, D-44 / #5368), the same as an
108
+ // explicit "" (the OSS-strip build target); a full allowlist is the
109
+ // every-module-on case. A slice route absent under "" must be absent from
110
+ // BOTH surfaces there, which is what makes its absence agreement rather than
111
+ // drift.
112
+ const ALL_PAID_MODULES =
113
+ "docs,multi-tenant,scheduling,content-write,signal-collectors";
114
+ const CONFIGS: Array<[string, string | undefined]> = [
115
+ ["AILK_ENABLED_MODULES unset (no paid module, the default)", undefined],
116
+ ['AILK_ENABLED_MODULES="" (no paid module)', ""],
117
+ [
118
+ `AILK_ENABLED_MODULES="${ALL_PAID_MODULES}" (every paid module on)`,
119
+ ALL_PAID_MODULES,
120
+ ],
121
+ ];
122
+
123
+ describe("API self-description drift (#5314)", () => {
124
+ describe.each(CONFIGS)("%s", (_label, enabled) => {
125
+ let surfaces: Surfaces;
126
+
127
+ beforeAll(async () => {
128
+ surfaces = await surfacesFor(enabled);
129
+ }, 60000);
130
+
131
+ // Anti-vacuity floor. A coverage assertion over an empty registry passes
132
+ // trivially, so an empty or stale registry is failed directly — the most
133
+ // likely way a derivation gate silently stops testing anything.
134
+ it("registers a non-empty route table including the floor route", () => {
135
+ expect(surfaces.registered.length).toBeGreaterThan(0);
136
+ const keys = surfaces.registered.map((r) => routeKey(r.method, r.url));
137
+ expect(keys).toContain(FLOOR_ROUTE);
138
+ expect(new Set(keys).size).toBe(keys.length);
139
+ });
140
+
141
+ it("publishes no surface entry for a route the server does not register", () => {
142
+ const diffs = compareSurfaces(
143
+ surfaces.registered,
144
+ surfaces.paths,
145
+ surfaces.endpoints,
146
+ );
147
+ const orphaned = diffs.filter((d) => d.orphanedInSurface.length > 0);
148
+ expect(formatSurfaceDrift(orphaned)).toBe("");
149
+ });
150
+
151
+ it("describes every registered route on both surfaces", () => {
152
+ const diffs = compareSurfaces(
153
+ surfaces.registered,
154
+ surfaces.paths,
155
+ surfaces.endpoints,
156
+ );
157
+ expect(formatSurfaceDrift(diffs)).toBe("");
158
+ expect(hasDrift(diffs)).toBe(false);
159
+ });
160
+
161
+ // AC-1 names this endpoint specifically: it returns 200 and is what the
162
+ // consuming site depends on, and before #5314 both surfaces omitted it.
163
+ // #5366 (D-48) moved it to signal-collectors: the no-drift half
164
+ // (`missingFromSurface`) holds regardless of whether the route
165
+ // registers at all, but the route itself now only registers when
166
+ // signal-collectors is one of the enabled modules for this config.
167
+ it("describes GET /v1/project-listings/public on both surfaces when registered", () => {
168
+ const diffs = compareSurfaces(
169
+ surfaces.registered,
170
+ surfaces.paths,
171
+ surfaces.endpoints,
172
+ );
173
+ for (const d of diffs) {
174
+ expect(d.missingFromSurface).not.toContain(
175
+ routeKey("GET", "/v1/project-listings/public"),
176
+ );
177
+ }
178
+ const signalCollectorsEnabled = (enabled ?? "")
179
+ .split(",")
180
+ .map((s) => s.trim())
181
+ .includes("signal-collectors");
182
+ expect(
183
+ surfaces.registered.some(
184
+ (r) => r.url === "/v1/project-listings/public" && r.method === "GET",
185
+ ),
186
+ ).toBe(signalCollectorsEnabled);
187
+ });
188
+ });
189
+
190
+ // Regression: the route table is per-instance, not module state.
191
+ //
192
+ // `vercel-handler.ts`'s `getServer()` is a check-then-set — it tests
193
+ // `!serverInstance`, then awaits twice before assigning — so two concurrent
194
+ // invocations can both call `build()`. With a module-level table, one
195
+ // build's reset would land mid-fill of the other and leave a truncated or
196
+ // doubled set, and both surfaces would silently misreport. Building two
197
+ // servers under DIFFERENT module configs and asserting each keeps its own
198
+ // table is what makes that unreachable rather than merely unlikely.
199
+ it("keeps each server's route table on its own instance", async () => {
200
+ const prior = process.env.AILK_ENABLED_MODULES;
201
+ try {
202
+ process.env.AILK_ENABLED_MODULES = ALL_PAID_MODULES;
203
+ const allOn = await build();
204
+ await allOn.ready();
205
+
206
+ process.env.AILK_ENABLED_MODULES = "";
207
+ const noneOn = await build();
208
+ await noneOn.ready();
209
+
210
+ const allKeys = readRegisteredRoutes(allOn).map((r) =>
211
+ routeKey(r.method, r.url),
212
+ );
213
+ const noneKeys = readRegisteredRoutes(noneOn).map((r) =>
214
+ routeKey(r.method, r.url),
215
+ );
216
+
217
+ // The second build must not have emptied, truncated or doubled the
218
+ // first — the exact corruption a shared table would produce.
219
+ expect(allKeys.length).toBeGreaterThan(noneKeys.length);
220
+ expect(new Set(allKeys).size).toBe(allKeys.length);
221
+ expect(new Set(noneKeys).size).toBe(noneKeys.length);
222
+ expect(allKeys).toContain(FLOOR_ROUTE);
223
+ expect(noneKeys).toContain(FLOOR_ROUTE);
224
+
225
+ // And each server's own OpenAPI document still matches its own table.
226
+ for (const app of [allOn, noneOn]) {
227
+ const paths = JSON.parse(
228
+ (await app.inject({ method: "GET", url: "/v1/openapi.json" })).body,
229
+ ).paths;
230
+ const endpoints = JSON.parse(
231
+ (await app.inject({ method: "GET", url: "/" })).body,
232
+ ).endpoints;
233
+ expect(
234
+ formatSurfaceDrift(
235
+ compareSurfaces(readRegisteredRoutes(app), paths, endpoints),
236
+ ),
237
+ ).toBe("");
238
+ }
239
+
240
+ await allOn.close();
241
+ await noneOn.close();
242
+ } finally {
243
+ if (prior === undefined) {
244
+ delete process.env.AILK_ENABLED_MODULES;
245
+ } else {
246
+ process.env.AILK_ENABLED_MODULES = prior;
247
+ }
248
+ }
249
+ }, 60000);
250
+
251
+ // AC-4. Asserted as agreement rather than as an exemption list: a disabled
252
+ // module's routes vanish from the registered set and from both surfaces at
253
+ // once, so there is nothing for the gate to forgive.
254
+ it("drops a disabled module's routes from the route table AND both surfaces together", async () => {
255
+ const allOn = await surfacesFor(ALL_PAID_MODULES);
256
+ const noneOn = await surfacesFor("");
257
+
258
+ const noneKeys = new Set(
259
+ noneOn.registered.map((r) => routeKey(r.method, r.url)),
260
+ );
261
+ const moduleOnly = allOn.registered.filter(
262
+ (r) => !noneKeys.has(routeKey(r.method, r.url)),
263
+ );
264
+
265
+ // The configurations must actually differ, or this asserts nothing.
266
+ expect(moduleOnly.length).toBeGreaterThan(0);
267
+
268
+ // Every module-only route is described when its module is on...
269
+ expect(
270
+ formatSurfaceDrift(
271
+ compareSurfaces(allOn.registered, allOn.paths, allOn.endpoints),
272
+ ),
273
+ ).toBe("");
274
+
275
+ // ...and named by neither surface when it is off, which is agreement.
276
+ //
277
+ // Each surface is parsed by the instrument's own reader, and each route is
278
+ // compared in that surface's vocabulary — OpenAPI-templated for `paths`,
279
+ // Fastify-shaped for `endpoints`. Comparing raw values instead would make
280
+ // this assertion quietly weaker than it reads: `paths` keys `{id}` where a
281
+ // registered route says `:id`, so every parametric route would "pass" on a
282
+ // vocabulary mismatch, and `endpoints` groups methods into one value
283
+ // (`"GET/PUT/DELETE /v1/user/data/:key"`), which never equals a
284
+ // single-method key. Both halves would then be vacuous for exactly the
285
+ // routes most likely to regress.
286
+ const offPathKeys = openApiSurfaceKeys(noneOn.paths);
287
+ const offEndpointKeys = rootIndexSurfaceKeys(noneOn.endpoints);
288
+ for (const { method, url } of moduleOnly) {
289
+ expect(offPathKeys).not.toContain(routeKey(method, toOpenApiPath(url)));
290
+ expect(offEndpointKeys).not.toContain(routeKey(method, url));
291
+ }
292
+ }, 60000);
293
+ });
@@ -3,10 +3,16 @@
3
3
  */
4
4
  import type { FastifyPluginAsync } from "fastify";
5
5
 
6
- import { buildOpenApiSpec } from "./spec.js";
6
+ import { buildOpenApiSpec, readRegisteredRoutes } from "./spec.js";
7
7
 
8
8
  export const openapiRoutes: FastifyPluginAsync = async (fastify) => {
9
- fastify.get("/v1/openapi.json", async (_request, reply) => {
10
- return reply.send(buildOpenApiSpec());
9
+ fastify.get("/v1/openapi.json", async (request, reply) => {
10
+ // The route table comes off the instance that served this request (#5314),
11
+ // never from module state — two servers built concurrently in one process
12
+ // would otherwise share, and corrupt, one table. Absent (a bare Fastify
13
+ // instance with no `build()`) degrades to the hand-authored paths.
14
+ return reply.send(
15
+ buildOpenApiSpec(undefined, readRegisteredRoutes(request.server)),
16
+ );
11
17
  });
12
18
  };