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,195 @@
1
+ /**
2
+ * The comparison instrument for #5314 — diffs the routes the server ACTUALLY
3
+ * registers against each surface that claims to describe them.
4
+ *
5
+ * Two surfaces describe this API's endpoints: the root index `endpoints`
6
+ * object (`server.ts`) and the OpenAPI `paths` object (`spec.ts`). Before
7
+ * #5314 both were hand-maintained literals, and both had drifted — 11 and 7
8
+ * entries against 68 registered routes, with `/v1/project-listings/public`
9
+ * missing from both and `/v1/billing/usage` missing from the root index.
10
+ *
11
+ * This module only COMPARES. The derivation that keeps the surfaces honest
12
+ * lives in `spec.ts` beside the shapes it produces; keeping the comparison
13
+ * separate is what lets the gate assert against a surface it did not build.
14
+ *
15
+ * Deliberately free of any `fastify` or `spec.ts` import: it takes plain data,
16
+ * so the gate can feed it a SERVED document (fetched over `inject`) rather
17
+ * than the object the code under test just returned.
18
+ */
19
+
20
+ /** One route as Fastify registered it. `url` is Fastify-shaped (`:param`). */
21
+ export interface RegisteredRoute {
22
+ /** Uppercase HTTP method. HEAD is excluded — Fastify derives it from GET. */
23
+ method: string;
24
+ /** Fastify path, e.g. `/v1/content/:type/:slug`. */
25
+ url: string;
26
+ }
27
+
28
+ /** One surface's disagreement with the registered route table. */
29
+ export interface SurfaceDiff {
30
+ /** Which surface this diff describes. */
31
+ surface: "openapi-paths" | "root-index-endpoints";
32
+ /** `METHOD /path` registered by the server but absent from the surface. */
33
+ missingFromSurface: string[];
34
+ /** `METHOD /path` named by the surface but not registered by the server. */
35
+ orphanedInSurface: string[];
36
+ }
37
+
38
+ /**
39
+ * Fastify path → OpenAPI path template: `/v1/content/:type` becomes
40
+ * `/v1/content/{type}`. OpenAPI 3.1 has no `:param` form, so a document that
41
+ * published the Fastify string would be describing a path no client can build.
42
+ */
43
+ export function toOpenApiPath(fastifyPath: string): string {
44
+ return fastifyPath
45
+ .split("/")
46
+ .map((segment) =>
47
+ segment.startsWith(":") ? `{${segment.slice(1)}}` : segment,
48
+ )
49
+ .join("/");
50
+ }
51
+
52
+ /** The comparison key both sides normalise to: `GET /v1/content/types`. */
53
+ export function routeKey(method: string, path: string): string {
54
+ return `${method.toUpperCase()} ${path}`;
55
+ }
56
+
57
+ /** Registered routes as OpenAPI-shaped comparison keys. */
58
+ function registeredOpenApiKeys(routes: RegisteredRoute[]): Set<string> {
59
+ return new Set(routes.map((r) => routeKey(r.method, toOpenApiPath(r.url))));
60
+ }
61
+
62
+ /** Registered routes as Fastify-shaped comparison keys. */
63
+ function registeredFastifyKeys(routes: RegisteredRoute[]): Set<string> {
64
+ return new Set(routes.map((r) => routeKey(r.method, r.url)));
65
+ }
66
+
67
+ /**
68
+ * The OpenAPI methods a path item declares. Non-method keys an OpenAPI path
69
+ * item may legally carry (`parameters`, `summary`, `description`, `servers`,
70
+ * `$ref`) are not operations and must not be compared as if they were.
71
+ */
72
+ const OPENAPI_METHODS = new Set([
73
+ "get",
74
+ "put",
75
+ "post",
76
+ "delete",
77
+ "options",
78
+ "head",
79
+ "patch",
80
+ "trace",
81
+ ]);
82
+
83
+ /**
84
+ * Comparison keys a served OpenAPI `paths` object claims.
85
+ *
86
+ * Takes the served document's `paths` as plain data so the gate can compare
87
+ * what an agent would actually fetch, not what the builder returned.
88
+ */
89
+ export function openApiSurfaceKeys(
90
+ paths: Record<string, Record<string, unknown>>,
91
+ ): Set<string> {
92
+ const keys = new Set<string>();
93
+ for (const [path, item] of Object.entries(paths ?? {})) {
94
+ for (const method of Object.keys(item ?? {})) {
95
+ if (!OPENAPI_METHODS.has(method.toLowerCase())) continue;
96
+ if (method.toLowerCase() === "head") continue;
97
+ keys.add(routeKey(method, path));
98
+ }
99
+ }
100
+ return keys;
101
+ }
102
+
103
+ /**
104
+ * Comparison keys a root-index `endpoints` object claims.
105
+ *
106
+ * Each value is `"METHOD[/METHOD...] /path"` — the shape the hand-authored
107
+ * object already used (`"GET/PATCH /v1/user/profile"`), preserved so no
108
+ * published entry changes shape. A value that does not parse is reported as
109
+ * an orphan rather than skipped: an unparseable entry is precisely the kind
110
+ * of hand-edit this instrument exists to catch.
111
+ */
112
+ export function rootIndexSurfaceKeys(
113
+ endpoints: Record<string, string>,
114
+ ): Set<string> {
115
+ const keys = new Set<string>();
116
+ for (const value of Object.values(endpoints ?? {})) {
117
+ const match = /^([A-Za-z/]+)\s+(\S+)$/.exec(value ?? "");
118
+ if (!match?.[1] || !match[2]) {
119
+ keys.add(`UNPARSEABLE ${value}`);
120
+ continue;
121
+ }
122
+ for (const method of match[1].split("/")) {
123
+ if (!method) continue;
124
+ if (method.toUpperCase() === "HEAD") continue;
125
+ keys.add(routeKey(method, match[2]));
126
+ }
127
+ }
128
+ return keys;
129
+ }
130
+
131
+ function diff(registered: Set<string>, surface: Set<string>) {
132
+ return {
133
+ missingFromSurface: [...registered].filter((k) => !surface.has(k)).sort(),
134
+ orphanedInSurface: [...surface].filter((k) => !registered.has(k)).sort(),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Diff the registered route table against both surfaces.
140
+ *
141
+ * `paths` is the served OpenAPI `paths` object (OpenAPI-templated); `endpoints`
142
+ * is the served root-index `endpoints` object (Fastify-shaped, matching the
143
+ * strings that surface has always published). Each is compared in its own
144
+ * vocabulary so neither surface is judged against a shape it never used.
145
+ */
146
+ export function compareSurfaces(
147
+ registered: RegisteredRoute[],
148
+ paths: Record<string, Record<string, unknown>>,
149
+ endpoints: Record<string, string>,
150
+ ): SurfaceDiff[] {
151
+ return [
152
+ {
153
+ surface: "openapi-paths",
154
+ ...diff(registeredOpenApiKeys(registered), openApiSurfaceKeys(paths)),
155
+ },
156
+ {
157
+ surface: "root-index-endpoints",
158
+ ...diff(
159
+ registeredFastifyKeys(registered),
160
+ rootIndexSurfaceKeys(endpoints),
161
+ ),
162
+ },
163
+ ];
164
+ }
165
+
166
+ /** Whether any surface disagrees with the registered route table. */
167
+ export function hasDrift(diffs: SurfaceDiff[]): boolean {
168
+ return diffs.some(
169
+ (d) => d.missingFromSurface.length > 0 || d.orphanedInSurface.length > 0,
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Render the drift for a failing assertion, or `""` when there is none.
175
+ *
176
+ * A surface that agrees contributes nothing, so the empty string means "no
177
+ * drift" and can be asserted against directly. Counts come first so a reader
178
+ * sees the scale before the list, then EVERY entry — a truncated drift report
179
+ * is how a gate teaches people to ignore it.
180
+ */
181
+ export function formatSurfaceDrift(diffs: SurfaceDiff[]): string {
182
+ const lines: string[] = [];
183
+ for (const d of diffs) {
184
+ if (d.missingFromSurface.length === 0 && d.orphanedInSurface.length === 0) {
185
+ continue;
186
+ }
187
+ lines.push(
188
+ `${d.surface}: ${d.missingFromSurface.length} registered route(s) missing from the surface, ` +
189
+ `${d.orphanedInSurface.length} surface entr(ies) with no registered route`,
190
+ );
191
+ for (const k of d.missingFromSurface) lines.push(` missing: ${k}`);
192
+ for (const k of d.orphanedInSurface) lines.push(` orphaned: ${k}`);
193
+ }
194
+ return lines.join("\n");
195
+ }
@@ -61,14 +61,28 @@ const validBody = {
61
61
 
62
62
  describe("POST /api/content/:type", () => {
63
63
  let app: FastifyInstance;
64
+ // The write handlers only register when content-write is enabled
65
+ // (routes/content/index.ts's isModuleEnabled('content-write') gate); an
66
+ // unset AILK_ENABLED_MODULES no longer defaults to every module on
67
+ // (D-44, #5368), so this suite enables it explicitly.
68
+ const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
64
69
 
65
70
  beforeEach(async () => {
71
+ process.env.AILK_ENABLED_MODULES = "content-write";
66
72
  app = require("fastify")();
67
73
  await app.register(contentRoutes);
68
74
  jest.clearAllMocks();
69
75
  mockCheckLicense.mockReset();
70
76
  });
71
77
 
78
+ afterAll(() => {
79
+ if (priorEnabledModules === undefined) {
80
+ delete process.env.AILK_ENABLED_MODULES;
81
+ } else {
82
+ process.env.AILK_ENABLED_MODULES = priorEnabledModules;
83
+ }
84
+ });
85
+
72
86
  afterEach(async () => {
73
87
  await app.close();
74
88
  });
@@ -49,14 +49,28 @@ function freeLicense() {
49
49
 
50
50
  describe("DELETE /api/content/:type/:slug", () => {
51
51
  let app: FastifyInstance;
52
+ // The write handlers only register when content-write is enabled
53
+ // (routes/content/index.ts's isModuleEnabled('content-write') gate); an
54
+ // unset AILK_ENABLED_MODULES no longer defaults to every module on
55
+ // (D-44, #5368), so this suite enables it explicitly.
56
+ const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
52
57
 
53
58
  beforeEach(async () => {
59
+ process.env.AILK_ENABLED_MODULES = "content-write";
54
60
  app = require("fastify")();
55
61
  await app.register(contentRoutes);
56
62
  jest.clearAllMocks();
57
63
  mockCheckLicense.mockReset();
58
64
  });
59
65
 
66
+ afterAll(() => {
67
+ if (priorEnabledModules === undefined) {
68
+ delete process.env.AILK_ENABLED_MODULES;
69
+ } else {
70
+ process.env.AILK_ENABLED_MODULES = priorEnabledModules;
71
+ }
72
+ });
73
+
60
74
  afterEach(async () => {
61
75
  await app.close();
62
76
  });
@@ -61,14 +61,28 @@ const validBody = {
61
61
 
62
62
  describe("PATCH /api/content/:type/:slug", () => {
63
63
  let app: FastifyInstance;
64
+ // The write handlers only register when content-write is enabled
65
+ // (routes/content/index.ts's isModuleEnabled('content-write') gate); an
66
+ // unset AILK_ENABLED_MODULES no longer defaults to every module on
67
+ // (D-44, #5368), so this suite enables it explicitly.
68
+ const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
64
69
 
65
70
  beforeEach(async () => {
71
+ process.env.AILK_ENABLED_MODULES = "content-write";
66
72
  app = require("fastify")();
67
73
  await app.register(contentRoutes);
68
74
  jest.clearAllMocks();
69
75
  mockCheckLicense.mockReset();
70
76
  });
71
77
 
78
+ afterAll(() => {
79
+ if (priorEnabledModules === undefined) {
80
+ delete process.env.AILK_ENABLED_MODULES;
81
+ } else {
82
+ process.env.AILK_ENABLED_MODULES = priorEnabledModules;
83
+ }
84
+ });
85
+
72
86
  afterEach(async () => {
73
87
  await app.close();
74
88
  });
@@ -24,25 +24,52 @@
24
24
  import type { FastifyPluginAsync } from "fastify";
25
25
 
26
26
  import { isModuleEnabled } from "../../config/modules.js";
27
+ import { loadSlice } from "../../lib/slice-load.js";
27
28
 
28
29
  import { getPageRoute } from "./get-page.js";
29
30
  import { listPagesRoute } from "./list-pages.js";
30
31
  import { listTypesRoute } from "./list-types.js";
31
32
 
32
33
  /**
33
- * Content-write slice entry points, as { specifier, exportName }.
34
+ * Content-write slice entry points, as { specifier, exportName, load }.
34
35
  *
35
36
  * What defeats `tsc`'s resolution is the INDIRECTION, not the type: TypeScript
36
37
  * resolves `import("...")` only when the argument is a string-literal
37
- * EXPRESSION written at the call site. Reading the specifier from a variable
38
- * makes it unresolvable regardless of whether that variable's type is `string`
39
- * or a literal (verified both ways). `string` is used here for readability, not
40
- * because a literal type would leak.
38
+ * EXPRESSION written at the call site.
39
+ *
40
+ * WHICH indirection matters, and #5304 is the correction. This table used to
41
+ * hold the specifier as a string and the loop read it out per iteration — a
42
+ * shape `@vercel/nft` cannot evaluate either, so the deploy-time tracer did not
43
+ * emit these files and a deployed site shipped without them. A module-level
44
+ * constant dereferenced into `import()` is unresolvable to `tsc` and traceable
45
+ * to `nft`, so the strip and the deployment are both satisfied. `specifier` is
46
+ * carried for the drift message below and reads the same constant as `load`, so
47
+ * the two cannot disagree.
41
48
  */
42
- const WRITE_ROUTES: ReadonlyArray<{ specifier: string; exportName: string }> = [
43
- { specifier: "./create.js", exportName: "createPageRoute" },
44
- { specifier: "./update.js", exportName: "updatePageRoute" },
45
- { specifier: "./delete.js", exportName: "deletePageRoute" },
49
+ const CREATE_ROUTE = "./create.js";
50
+ const UPDATE_ROUTE = "./update.js";
51
+ const DELETE_ROUTE = "./delete.js";
52
+
53
+ const WRITE_ROUTES: ReadonlyArray<{
54
+ specifier: string;
55
+ exportName: string;
56
+ load: () => Promise<unknown>;
57
+ }> = [
58
+ {
59
+ specifier: CREATE_ROUTE,
60
+ exportName: "createPageRoute",
61
+ load: () => import(CREATE_ROUTE),
62
+ },
63
+ {
64
+ specifier: UPDATE_ROUTE,
65
+ exportName: "updatePageRoute",
66
+ load: () => import(UPDATE_ROUTE),
67
+ },
68
+ {
69
+ specifier: DELETE_ROUTE,
70
+ exportName: "deletePageRoute",
71
+ load: () => import(DELETE_ROUTE),
72
+ },
46
73
  ];
47
74
 
48
75
  export const contentRoutes: FastifyPluginAsync = async (fastify) => {
@@ -51,8 +78,8 @@ export const contentRoutes: FastifyPluginAsync = async (fastify) => {
51
78
  await fastify.register(getPageRoute);
52
79
 
53
80
  if (isModuleEnabled("content-write")) {
54
- for (const { specifier, exportName } of WRITE_ROUTES) {
55
- const mod = (await import(specifier)) as Record<
81
+ for (const { specifier, exportName, load } of WRITE_ROUTES) {
82
+ const mod = (await loadSlice(specifier, load)) as Record<
56
83
  string,
57
84
  FastifyPluginAsync | undefined
58
85
  >;
@@ -1,3 +1,6 @@
1
+ // Not marked oss: false (D-48/#5366 escalation) — tests the
2
+ // always-shipping project-listings anonymous surface; see
3
+ // routes/project-listings/me.ts's header comment.
1
4
  /**
2
5
  * @file configured-application.test.ts
3
6
  *
@@ -43,6 +46,13 @@ jest.mock("../../../lib/prisma.js");
43
46
 
44
47
  jest.mock("@working-theory/email", () => ({
45
48
  sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
49
+ // #5406 — `start` now reaches the resume email. Declared here for the reason
50
+ // docs/lessons/project-listings/04 records: a mock factory that omits an
51
+ // export the code under test calls leaves it `undefined` at the callsite.
52
+ // These suites configure no `resumeEmail`, so the send short-circuits before
53
+ // this is reached; it is declared so a future fixture that DOES configure
54
+ // one fails on its assertion rather than on a missing mock.
55
+ sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
46
56
  }));
47
57
 
48
58
  jest.mock("@working-theory/license", () => ({
@@ -217,6 +227,13 @@ function installStore(): void {
217
227
  anythingElse: null,
218
228
  address: null,
219
229
  questions: null,
230
+ // #5407 — a row written before the columns existed, and a freshly
231
+ // started draft on a site that asks for one name line: parts null,
232
+ // no phone, no verification stamp.
233
+ founderFirstName: null,
234
+ founderLastName: null,
235
+ founderPhone: null,
236
+ emailVerifiedAt: null,
220
237
  founderCompany: null,
221
238
  leadId: null,
222
239
  headline: null,
@@ -1,3 +1,6 @@
1
+ // Not marked oss: false (D-48/#5366 escalation) — tests the
2
+ // always-shipping project-listings anonymous surface; see
3
+ // routes/project-listings/me.ts's header comment.
1
4
  /**
2
5
  * @file drafts.test.ts
3
6
  *
@@ -40,6 +43,13 @@ jest.mock("../../../lib/prisma.js");
40
43
 
41
44
  jest.mock("@working-theory/email", () => ({
42
45
  sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
46
+ // #5406 — `start` now reaches the resume email. Declared here for the reason
47
+ // docs/lessons/project-listings/04 records: a mock factory that omits an
48
+ // export the code under test calls leaves it `undefined` at the callsite.
49
+ // These suites configure no `resumeEmail`, so the send short-circuits before
50
+ // this is reached; it is declared so a future fixture that DOES configure
51
+ // one fails on its assertion rather than on a missing mock.
52
+ sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
43
53
  }));
44
54
 
45
55
  jest.mock("@working-theory/license", () => ({
@@ -193,6 +203,13 @@ function installStore(): void {
193
203
  anythingElse: null,
194
204
  address: null,
195
205
  questions: null,
206
+ // #5407 — a row written before the columns existed, and a freshly
207
+ // started draft on a site that asks for one name line: parts null,
208
+ // no phone, no verification stamp.
209
+ founderFirstName: null,
210
+ founderLastName: null,
211
+ founderPhone: null,
212
+ emailVerifiedAt: null,
196
213
  founderCompany: null,
197
214
  leadId: null,
198
215
  headline: null,