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,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
  };