create-ailk 0.5.0 → 0.6.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 (299) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/lib/api-boot-assertion.d.ts +46 -0
  5. package/dist/lib/api-boot-assertion.js +70 -0
  6. package/dist/lib/import-graph.d.ts +29 -0
  7. package/dist/lib/import-graph.js +140 -0
  8. package/dist/module-architecture.d.ts +1 -0
  9. package/dist/module-architecture.js +253 -127
  10. package/dist/surfaces.js +105 -7
  11. package/dist/template-manifest.d.ts +33 -0
  12. package/dist/template-manifest.js +99 -16
  13. package/dist/template-names.d.ts +1 -1
  14. package/dist/template-names.js +1 -0
  15. package/package.json +3 -2
  16. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  17. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  18. package/site-templates/marketing-site/ailk-template.json +1 -1
  19. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  20. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  21. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  22. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  23. package/site-templates/minimalism-site/ailk-template.json +2 -1
  24. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  25. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  26. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  27. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  28. package/site-templates/simple-service/ailk-template.json +10 -0
  29. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  30. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  31. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  32. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  33. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  34. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  35. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  36. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  37. package/site-templates/simple-service/provenance.json +4 -0
  38. package/templates/.env.example +72 -7
  39. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  40. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  41. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  42. package/templates/apps/api/.env.example +13 -0
  43. package/templates/apps/api/CLAUDE.md +26 -2
  44. package/templates/apps/api/jest.config.cjs +3 -0
  45. package/templates/apps/api/package.json +1 -0
  46. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  47. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  48. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  49. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  50. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  51. package/templates/apps/api/src/config/index.ts +62 -0
  52. package/templates/apps/api/src/config/modules.ts +7 -3
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  54. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  55. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  56. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  57. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  58. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  59. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  60. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  61. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  62. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  63. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  64. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  65. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  66. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  67. package/templates/apps/api/src/lib/site.ts +208 -9
  68. package/templates/apps/api/src/lib/stripe.ts +66 -0
  69. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  70. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  71. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  72. package/templates/apps/api/src/openapi/spec.ts +543 -2
  73. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  74. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  75. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  76. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  77. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  78. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  80. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  81. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  82. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  83. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  84. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  85. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  87. package/templates/apps/api/src/routes/content/index.ts +13 -73
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  89. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  90. package/templates/apps/api/src/routes/content/search.ts +129 -0
  91. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  92. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  93. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  94. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  95. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  96. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  97. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  98. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  99. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  100. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  101. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  102. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  103. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  104. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  105. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  106. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  111. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  112. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  113. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  114. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  115. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  116. package/templates/apps/api/src/server.ts +439 -49
  117. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  118. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  119. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  120. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  121. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  122. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  123. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  124. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  125. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  126. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  127. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  128. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  129. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  130. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  131. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  132. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  133. package/templates/apps/api/src/services/audit-log.ts +668 -0
  134. package/templates/apps/api/src/services/consent.ts +18 -0
  135. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  136. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  137. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  138. package/templates/apps/api/src/services/entitlements.ts +80 -0
  139. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  140. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  141. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  142. package/templates/apps/api/src/services/listing-config.ts +27 -9
  143. package/templates/apps/api/src/services/project-listings.ts +201 -6
  144. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  145. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  146. package/templates/apps/api/src/services/revalidation.ts +173 -0
  147. package/templates/apps/api/src/services/verified-email.ts +71 -0
  148. package/templates/apps/api/src/vercel-handler.ts +75 -35
  149. package/templates/apps/mcp/.env.example +17 -0
  150. package/templates/apps/mcp/CLAUDE.md +57 -24
  151. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  152. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  153. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  154. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  155. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  156. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  157. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  158. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  159. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  160. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  161. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  162. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  163. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  164. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  165. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  166. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  167. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  168. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  169. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  170. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  171. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  172. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  173. package/templates/apps/mcp/jest.config.cjs +13 -0
  174. package/templates/apps/mcp/package.json +2 -0
  175. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  176. package/templates/apps/mcp/src/config/modules.ts +10 -5
  177. package/templates/apps/mcp/src/http-transport.ts +102 -29
  178. package/templates/apps/mcp/src/index.ts +73 -11
  179. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  180. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  181. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  182. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  183. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  184. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  185. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  186. package/templates/apps/mcp/src/server.ts +59 -35
  187. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  188. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  189. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  190. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  191. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  192. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  193. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  194. package/templates/apps/mcp/src/tools/index.ts +6 -4
  195. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  196. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  197. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  198. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  199. package/templates/apps/web/.env.example +71 -5
  200. package/templates/apps/web/CLAUDE.md +44 -0
  201. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  202. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  203. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  204. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  205. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  206. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  207. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  208. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  209. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  210. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  211. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  212. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  213. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  214. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  215. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  216. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  217. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  218. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  219. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  220. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  221. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  222. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  223. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  224. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  225. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  226. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  227. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  228. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  229. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  230. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  232. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  233. package/templates/apps/web/auth.ts +407 -8
  234. package/templates/apps/web/jest.config.cjs +8 -4
  235. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  236. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  237. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  238. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  239. package/templates/apps/web/lib/auth-email.ts +318 -0
  240. package/templates/apps/web/lib/auth-env.ts +85 -0
  241. package/templates/apps/web/lib/auth-routes.ts +55 -0
  242. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  243. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  244. package/templates/apps/web/messages/en.json +187 -0
  245. package/templates/apps/web/package.json +2 -1
  246. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  247. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  248. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  249. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  250. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  251. package/templates/database/CHANGELOG.md +142 -0
  252. package/templates/database/README.md +10 -3
  253. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  254. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  255. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  256. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  257. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  258. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  259. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  260. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  261. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  262. package/templates/database/__tests__/tenancy.test.ts +141 -0
  263. package/templates/database/config/schema.prisma +79 -2
  264. package/templates/database/content/schema.prisma +87 -11
  265. package/templates/database/inbox/schema.prisma +452 -9
  266. package/templates/database/jest.config.cjs +5 -3
  267. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  268. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  269. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  270. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  271. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  272. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  273. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  274. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  275. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  276. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  277. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  278. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  279. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  280. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  281. package/templates/database/package.json +1 -1
  282. package/templates/database/tenancy.ts +86 -1
  283. package/templates/database/tsconfig.json +1 -0
  284. package/versions.json +34 -34
  285. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  286. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  287. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  288. package/templates/apps/api/src/routes/content/create.ts +0 -115
  289. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  290. package/templates/apps/api/src/routes/content/update.ts +0 -112
  291. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  292. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  293. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  294. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  295. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  296. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  297. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  298. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  299. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -16,11 +16,21 @@
16
16
 
17
17
  import { join } from "node:path";
18
18
 
19
- import { type AuditInput } from "@working-theory/aeo";
20
- import { createMdxAdapter, type SchemaOrgType } from "@working-theory/content-adapters";
19
+ import {
20
+ READINESS_SCAN_TYPES,
21
+ type AuditInput,
22
+ type ReadinessPageFacts,
23
+ } from "@working-theory/aeo";
24
+ import {
25
+ type ContentAdapter,
26
+ type ContentPage,
27
+ type SchemaOrgType,
28
+ } from "@working-theory/content-adapters";
21
29
  import { buildJsonLd, pageTypeRegistry } from "@working-theory/schema";
22
30
 
31
+ import { getMcpContentAdapter } from "./content-adapter.js";
23
32
  import { resolveLocale } from "./resolve-locale.js";
33
+ import { readStripeTaxSignal } from "./stripe-tax-signal.js";
24
34
 
25
35
  export interface SlugInput {
26
36
  slug: string;
@@ -121,15 +131,86 @@ function extractTypeSlugFromUrl(raw: string): {
121
131
  return { type, slug };
122
132
  }
123
133
 
134
+ /**
135
+ * The site-level view the readiness rules grade (#5949 FR9/D5).
136
+ *
137
+ * Five adapter calls, bounded and type-closed: the collection-style pair for
138
+ * `Product` / `Service`, the landing-style pair for the three policy types.
139
+ * Independent of site size and of which page was audited (NFR4). The five
140
+ * are independent of each other, so they run concurrently (#6012).
141
+ *
142
+ * Fail-open-to-pass, stated: under an unresolved adapter every list is `[]`,
143
+ * `sitePages` is empty, and all three rules pass — a site with no known priced
144
+ * page owes no policy page, which is the correct verdict for "the audit could
145
+ * not see the site". A type the adapter legitimately has no pages of resolves
146
+ * this way too, silently — it is not an error.
147
+ *
148
+ * A GENUINE adapter failure (the call throws) is a different condition from
149
+ * that silent "no pages of this type" case, and collapsing the two loses the
150
+ * distinction: never a throw that would take down an audit of an unrelated
151
+ * page, but the failure is surfaced rather than vanishing indistinguishably
152
+ * into the same vacuous pass (#6012, from the #6010 review).
153
+ *
154
+ * Logged via `console.error`, NOT `console.debug`/`console.log` — on the
155
+ * default stdio MCP transport (`apps/mcp/src/index.ts`), stdout IS the
156
+ * JSON-RPC framing channel, and `console.debug`/`console.log` both write
157
+ * there (they are the same stream in Node). `console.error` (stderr) is the
158
+ * convention this file's own bootstrap already uses for exactly this reason
159
+ * (`index.ts:43,63`) — writing anywhere else would corrupt the protocol
160
+ * stream for a mere adapter hiccup on one readiness-scan type.
161
+ */
162
+ async function scanSitePages(
163
+ adapter: ContentAdapter,
164
+ locale: string,
165
+ ): Promise<ReadinessPageFacts[]> {
166
+ const perType = await Promise.all(
167
+ READINESS_SCAN_TYPES.map(async (type) => {
168
+ const isDetail = type === "Product" || type === "Service";
169
+ try {
170
+ const pages = isDetail
171
+ ? await adapter.listDetail(type, locale)
172
+ : await adapter.listPages(type, locale);
173
+ return { type, pages };
174
+ } catch (err) {
175
+ console.error(
176
+ `[aeo] readiness scan: adapter threw listing type "${type}" — ` +
177
+ "treating as no pages for it",
178
+ err,
179
+ );
180
+ return { type, pages: [] as ContentPage[] };
181
+ }
182
+ }),
183
+ );
184
+
185
+ const facts: ReadinessPageFacts[] = [];
186
+ for (const { type, pages } of perType) {
187
+ for (const page of pages) {
188
+ facts.push({
189
+ slug: page.slug,
190
+ type,
191
+ ...(page.price !== undefined && { price: page.price }),
192
+ ...(page.stripe?.taxCode !== undefined && {
193
+ taxCode: page.stripe.taxCode,
194
+ }),
195
+ });
196
+ }
197
+ }
198
+ return facts;
199
+ }
200
+
124
201
  /**
125
202
  * Builds an `AuditInput` for a single page.
126
203
  *
127
204
  * Accepts an optional `contentRoot` override for testing. When omitted,
128
205
  * resolves the content root relative to cwd (supports ESM runtime).
206
+ *
207
+ * `projectRoot` is the matching seam for the project.yaml the Stripe-Tax signal
208
+ * is read from (#5949 D9); it likewise defaults to the process cwd.
129
209
  */
130
210
  export async function buildSinglePageAuditInput(
131
211
  input: SinglePageInput,
132
212
  contentRoot?: string,
213
+ projectRoot?: string,
133
214
  ): Promise<AuditInput> {
134
215
  // Validate: exactly one of slug or url, not both, not neither.
135
216
  const hasSlug = "slug" in input;
@@ -146,7 +227,7 @@ export async function buildSinglePageAuditInput(
146
227
 
147
228
  const root = contentRoot ?? join(process.cwd(), "content");
148
229
  const locale = resolveLocale(input.locale);
149
- const adapter = createMdxAdapter({ contentRoot: root, defaultLocale: locale });
230
+ const adapter = getMcpContentAdapter({ contentRoot: root, locale });
150
231
 
151
232
  let type: SchemaOrgType;
152
233
  let slug: string;
@@ -184,11 +265,17 @@ export async function buildSinglePageAuditInput(
184
265
  // This mirrors packages/aeo/src/cli/build-input.ts behavior.
185
266
  }
186
267
 
268
+ const stripeTaxEnabled = readStripeTaxSignal(projectRoot ?? process.cwd());
269
+
187
270
  return {
188
271
  pages: [{ slug, type, locale }],
189
272
  locale,
190
273
  identityMode: DEFAULT_IDENTITY_MODE,
191
274
  registry: pageTypeRegistry,
192
275
  jsonLdByPage,
276
+ readiness: {
277
+ sitePages: await scanSitePages(adapter, locale),
278
+ ...(stripeTaxEnabled !== undefined && { stripeTaxEnabled }),
279
+ },
193
280
  };
194
281
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The deployment's `commerce` add-on claim, resolved ONCE at boot and read
3
+ * synchronously thereafter (#5971 spec D2).
4
+ *
5
+ * Why memoised: `createMcpServer()` and `listRegisteredTools()`
6
+ * (`apps/mcp/src/server.ts`) are synchronous and must stay that way —
7
+ * `checkLicense()` is not. `lib/public-workspace.ts` (#5876) already solved
8
+ * this exact shape for the content adapter, and this file follows it
9
+ * verbatim rather than inventing a second idiom: `apps/mcp/src/index.ts`
10
+ * awaits the initialiser once, before either transport branch, and every
11
+ * reader takes the memoised value.
12
+ *
13
+ * Why TWO states where `apps/api/src/lib/require-commerce-claim.ts` has
14
+ * three: that file's `undetermined` arm exists for exactly one caller — the
15
+ * Stripe webhook receiver, which must hold an event with a retryable 503
16
+ * rather than drop a charge that already moved. `start_purchase` is the twin
17
+ * of the checkout LAUNCHER, not the webhook, and the launcher answers the
18
+ * module-disabled response on both non-serving states because a checkout that
19
+ * cannot be started loses nothing. So `undetermined` collapses into
20
+ * `unclaimed` at the boundary, and this gate needs neither the stored-JWT read
21
+ * nor the env-presence read the API twin carries.
22
+ *
23
+ * Fail closed (NFR-3): the initial state is `"unclaimed"`, and every path that
24
+ * cannot POSITIVELY establish the claim — an unresolved boot, a thrown
25
+ * `checkLicense()`, a revoked licence, an expired or unreadable one — leaves
26
+ * it that way.
27
+ *
28
+ * `apps/*` may not import `apps/*`, so the small duplication of the API-side
29
+ * gate is the same shape `apps/mcp/src/config/modules.ts` already documents
30
+ * for `isModuleEnabled`.
31
+ */
32
+
33
+ import { checkLicense } from "@working-theory/license";
34
+
35
+ /** The licence `features[]` claim that turns the commerce add-on on — the
36
+ * same literal `apps/api/src/lib/require-commerce-claim.ts` reads. */
37
+ const COMMERCE_FEATURE = "commerce";
38
+
39
+ /** Two states: the claim is positively held, or it is not. */
40
+ export type McpCommerceClaim = "claimed" | "unclaimed";
41
+
42
+ let claim: McpCommerceClaim = "unclaimed";
43
+
44
+ export interface InitMcpCommerceClaimOptions {
45
+ /** Injection seam for tests — when supplied, used in place of the real
46
+ * `checkLicense()` so no suite reaches a licence server (NFR-6). */
47
+ checkLicense?: typeof checkLicense;
48
+ }
49
+
50
+ /**
51
+ * Resolves the deployment's commerce claim, once, and memoises it for
52
+ * {@link mcpCommerceClaim}'s synchronous read. Safe to call more than once
53
+ * (each call re-resolves and re-memoises); production calls it exactly once,
54
+ * at bootstrap (`index.ts`).
55
+ *
56
+ * Never throws: a licence read that fails resolves to the fail-closed
57
+ * `"unclaimed"` rather than propagating.
58
+ */
59
+ export async function initMcpCommerceClaim(
60
+ options: InitMcpCommerceClaimOptions = {},
61
+ ): Promise<McpCommerceClaim> {
62
+ const read = options.checkLicense ?? checkLicense;
63
+ try {
64
+ const result = await read();
65
+ // Revoked paid licences keep verifying until JWT exp (up to 24h after the
66
+ // server reports `revoked`) — the same window `lib/require-paid-tier.ts`
67
+ // closes. See packages/license/src/types.ts § LicenseResult, ADR 0014.
68
+ claim =
69
+ result.revoked !== true && result.features.includes(COMMERCE_FEATURE)
70
+ ? "claimed"
71
+ : "unclaimed";
72
+ } catch {
73
+ claim = "unclaimed";
74
+ }
75
+ return claim;
76
+ }
77
+
78
+ /** The memoised claim, read synchronously by both gates in `server.ts` and
79
+ * by `tools/purchase.ts`'s call-time re-check. */
80
+ export function mcpCommerceClaim(): McpCommerceClaim {
81
+ return claim;
82
+ }
83
+
84
+ /** Test-only injector — sets the memoised claim directly, bypassing the
85
+ * licence read entirely. The seam every claim-state matrix in
86
+ * `apps/mcp/__tests__` drives both halves through. */
87
+ export function __setMcpCommerceClaimForTest(next: McpCommerceClaim): void {
88
+ claim = next;
89
+ }
90
+
91
+ /** Test-only reset — returns to the module's initial, fail-closed state. */
92
+ export function resetMcpCommerceClaim(): void {
93
+ claim = "unclaimed";
94
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The one content-adapter seam for apps/mcp (#5805).
3
+ *
4
+ * Adapter selection is NOT decided here — it is `resolveContentAdapter()`'s job
5
+ * (#5804, roadmap D3: one resolver, one env var). This module supplies the MCP
6
+ * server's own inputs (content root, resolved locale) and nothing else, so the
7
+ * twelve content tools and `audit-single-page` no longer each build their own
8
+ * MDX adapter against their own root. Mirrors
9
+ * `apps/api/src/lib/content-adapter.ts`, which adopted the resolver in #5827.
10
+ *
11
+ * `CONTENT_ADAPTER` selects: `mdx` (default) | `projection` | `database`.
12
+ * `CONTENT_BACKEND` is read for one more release by the resolver's shim.
13
+ *
14
+ * `CONTENT_ADAPTER=database` (#5876 spec D2/D4/D5/D6/D13). The `mdx` and
15
+ * `projection` paths below are byte-identical to before this issue — no
16
+ * `workspaceId`, no `await import()`, no workspace read (D6, NFR-1/NFR-2). On
17
+ * `database`, this seam reads `mcpPublicWorkspace()` — the resolution
18
+ * `apps/mcp/src/index.ts` already awaited once at bootstrap
19
+ * (`public-workspace.ts`, D4) — and forwards `workspaceId` + `prisma` to the
20
+ * resolver on a resolved deployment; every other arm (unresolved, or a tool
21
+ * invoked before bootstrap resolved) returns the fail-closed
22
+ * `emptyContentAdapter()` (D5), never a client-supplied workspace, never a
23
+ * throw. On BOTH arms the returned adapter is READ-ONLY under `database`:
24
+ * `createPage`/`updatePage`/`deletePage`/`getPageVersion` throw the named
25
+ * write error (D13) — this seam also serves `apps/mcp/src/tools/create_page.ts`
26
+ * et al. when they exist, so the boundary is held here, not per tool.
27
+ *
28
+ * The `contentRoot` argument is the test-override seam every tool already
29
+ * exposes as its optional second parameter — tests inject a fixture root
30
+ * through it, and that keeps working unchanged.
31
+ */
32
+
33
+ import { join } from "node:path";
34
+
35
+ import {
36
+ resolveContentAdapter,
37
+ resolvedAdapterKind,
38
+ type ContentAdapter,
39
+ type MdxContentAdapter,
40
+ } from "@working-theory/content-adapters";
41
+
42
+ import {
43
+ emptyContentAdapter,
44
+ readOnlyContentAdapter,
45
+ } from "./empty-content-adapter.js";
46
+ import { mcpPublicWorkspace } from "./public-workspace.js";
47
+
48
+ export interface McpContentAdapterOptions {
49
+ /** Filesystem content root. Defaults to `<cwd>/content` — the tools' prior default. */
50
+ contentRoot?: string;
51
+ /** Locale already resolved against the project allowlist (`resolveLocale()`). */
52
+ locale?: string;
53
+ }
54
+
55
+ /**
56
+ * Returns the `ContentAdapter` the environment selects, rooted at the caller's
57
+ * content root. The resolver memoises per distinct option set, so this is safe
58
+ * to call on every tool invocation.
59
+ */
60
+ export function getMcpContentAdapter(
61
+ options: McpContentAdapterOptions = {},
62
+ ): ContentAdapter {
63
+ const contentRoot = options.contentRoot ?? join(process.cwd(), "content");
64
+ const defaultLocale = options.locale;
65
+
66
+ if (resolvedAdapterKind() !== "database") {
67
+ return resolveContentAdapter({ contentRoot, defaultLocale });
68
+ }
69
+
70
+ const workspace = mcpPublicWorkspace();
71
+ if (workspace.kind !== "resolved") {
72
+ return emptyContentAdapter();
73
+ }
74
+
75
+ const adapter = resolveContentAdapter({
76
+ workspaceId: workspace.workspaceId,
77
+ prisma: workspace.prisma,
78
+ contentRoot,
79
+ defaultLocale,
80
+ });
81
+ // Under CONTENT_ADAPTER=database, resolveContentAdapter() always builds a
82
+ // DatabaseContentAdapter (packages/content-adapters/src/database/adapter.ts),
83
+ // which always exposes getPageSource — the read-only wrapper needs the
84
+ // MdxContentAdapter shape to pass the six reads + getPageSource through
85
+ // untouched and gate only the four write methods (D13).
86
+ return readOnlyContentAdapter(adapter as MdxContentAdapter);
87
+ }
88
+
89
+ /**
90
+ * The same resolution, narrowed to the MDX adapter's richer surface.
91
+ *
92
+ * `read_content` returns a page's raw MDX body, which only
93
+ * `MdxContentAdapter.getPageSource()` provides — the base `ContentAdapter`
94
+ * contract returns frontmatter only. Rather than cast, this asserts the
95
+ * capability on the adapter the resolver actually built, so a server pointed
96
+ * at a non-MDX `CONTENT_ADAPTER` gets a named error instead of a
97
+ * `getPageSource is not a function` TypeError. On the default `mdx` path the
98
+ * check always passes and behaviour is unchanged. Under `database`, both the
99
+ * fail-closed empty adapter and the read-only-wrapped database adapter also
100
+ * provide `getPageSource`, so the check passes there too (D5).
101
+ */
102
+ export function getMcpMdxContentAdapter(
103
+ options: McpContentAdapterOptions = {},
104
+ ): MdxContentAdapter {
105
+ const adapter = getMcpContentAdapter(options);
106
+ if (typeof (adapter as MdxContentAdapter).getPageSource !== "function") {
107
+ throw new Error(
108
+ "This tool needs the MDX adapter's getPageSource(): the adapter selected by " +
109
+ "CONTENT_ADAPTER does not provide a raw MDX body. Use CONTENT_ADAPTER=mdx (the default).",
110
+ );
111
+ }
112
+ return adapter as MdxContentAdapter;
113
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The fail-closed EMPTY adapter (#5876 spec D5) — a null object that holds
3
+ * no client and issues no query. `getMcpContentAdapter()` (`content-adapter.ts`)
4
+ * returns it whenever the environment selects `CONTENT_ADAPTER=database` and
5
+ * the deployment's workspace resolution (`public-workspace.ts`) did not land
6
+ * at `{ kind: "resolved" }`.
7
+ *
8
+ * All eleven `MdxContentAdapter` methods are specified, not left to throw a
9
+ * generic TypeError: the six reads plus `getPageSource` return their own
10
+ * not-found/empty shape, and the four write methods throw the SAME named
11
+ * error `readOnlyContentAdapter()` (below) throws on the RESOLVED arm
12
+ * (#5876 spec D13) — the seam is read-only under `CONTENT_ADAPTER=database`
13
+ * on both arms, not only this fail-closed one.
14
+ *
15
+ * Three properties follow for free from this shape, over the rejected
16
+ * alternatives (throwing the resolver's own "workspaceId required" error, or
17
+ * returning `null` from the seam and guarding per tool):
18
+ *
19
+ * 1. Every content tool's own not-found/empty mapping fires with NO tool
20
+ * edit — `read_content` and `get_*` already treat `null` as not-found;
21
+ * `list_pages` and `search_content` already treat `[]` as empty.
22
+ * 2. "Before any content lookup" (NFR-5) is structural, not asserted —
23
+ * there is nothing here to look anything up in.
24
+ * 3. `getMcpMdxContentAdapter()`'s `typeof getPageSource === "function"`
25
+ * capability check (`content-adapter.ts`) passes, so `read_content`
26
+ * never sees THAT named throw on the fail-closed path.
27
+ */
28
+ import type {
29
+ ContentPage,
30
+ ExtractionType,
31
+ MdxContentAdapter,
32
+ MdxPageSource,
33
+ SchemaOrgType,
34
+ SearchOptions,
35
+ SearchResult,
36
+ StructuredDataFor,
37
+ } from "@working-theory/content-adapters";
38
+
39
+ /**
40
+ * The one message every write method throws, on BOTH the empty adapter and
41
+ * the read-only wrapper of a resolved database adapter — the seam is
42
+ * read-only under `CONTENT_ADAPTER=database`, full stop (#5876 spec D13).
43
+ * The three write MCP tools this boundary was drawn against
44
+ * (`create_page`/`update_page`/`delete_page`) were retired ahead of this
45
+ * issue (#5853); this is the seam's own STOP boundary, held independent of
46
+ * which tools currently reach it.
47
+ */
48
+ const WRITE_NOT_SUPPORTED_MESSAGE =
49
+ "This MCP server's CONTENT_ADAPTER=database seam is read-only: " +
50
+ "createPage/updatePage/deletePage/getPageVersion are not available here. " +
51
+ "Content writes go through apps/api's tenant-guarded workspace content routes.";
52
+
53
+ function throwWriteNotSupported(): never {
54
+ throw new Error(WRITE_NOT_SUPPORTED_MESSAGE);
55
+ }
56
+
57
+ /**
58
+ * A null object satisfying `MdxContentAdapter`. Holds no client; every read
59
+ * method resolves synchronously to its empty shape with no I/O.
60
+ */
61
+ export function emptyContentAdapter(): MdxContentAdapter {
62
+ const adapter: MdxContentAdapter = {
63
+ async getPageBySlug(
64
+ _type: SchemaOrgType,
65
+ _slug: string,
66
+ _locale?: string,
67
+ ): Promise<ContentPage | null> {
68
+ return null;
69
+ },
70
+ async listPages(
71
+ _type: SchemaOrgType,
72
+ _locale?: string,
73
+ ): Promise<ContentPage[]> {
74
+ return [];
75
+ },
76
+ async getDetailBySlug(
77
+ _type: SchemaOrgType,
78
+ _slug: string,
79
+ _locale?: string,
80
+ ): Promise<ContentPage | null> {
81
+ return null;
82
+ },
83
+ async listDetail(
84
+ _type: SchemaOrgType,
85
+ _locale?: string,
86
+ ): Promise<ContentPage[]> {
87
+ return [];
88
+ },
89
+ async searchPages(
90
+ _query: string,
91
+ _options?: SearchOptions,
92
+ ): Promise<SearchResult[]> {
93
+ return [];
94
+ },
95
+ async extractStructuredData<T extends ExtractionType>(
96
+ _type: T,
97
+ _slug: string,
98
+ _locale?: string,
99
+ ): Promise<StructuredDataFor<T> | null> {
100
+ return null;
101
+ },
102
+ async getPageSource(
103
+ _type: SchemaOrgType,
104
+ _slug: string,
105
+ _locale?: string,
106
+ ): Promise<MdxPageSource | null> {
107
+ return null;
108
+ },
109
+ createPage: throwWriteNotSupported,
110
+ updatePage: throwWriteNotSupported,
111
+ deletePage: throwWriteNotSupported,
112
+ getPageVersion: throwWriteNotSupported,
113
+ };
114
+ return adapter;
115
+ }
116
+
117
+ /**
118
+ * Wraps a RESOLVED database adapter so its four write methods throw the same
119
+ * read-only error the empty adapter throws — the seam is read-only under
120
+ * `CONTENT_ADAPTER=database` on both arms (#5876 spec D13). The six read
121
+ * methods and `getPageSource` pass through untouched.
122
+ */
123
+ export function readOnlyContentAdapter(
124
+ adapter: MdxContentAdapter,
125
+ ): MdxContentAdapter {
126
+ return {
127
+ getPageBySlug: adapter.getPageBySlug.bind(adapter),
128
+ listPages: adapter.listPages.bind(adapter),
129
+ getDetailBySlug: adapter.getDetailBySlug.bind(adapter),
130
+ listDetail: adapter.listDetail.bind(adapter),
131
+ searchPages: adapter.searchPages.bind(adapter),
132
+ extractStructuredData: adapter.extractStructuredData.bind(adapter),
133
+ getPageSource: adapter.getPageSource.bind(adapter),
134
+ createPage: throwWriteNotSupported,
135
+ updatePage: throwWriteNotSupported,
136
+ deletePage: throwWriteNotSupported,
137
+ getPageVersion: throwWriteNotSupported,
138
+ };
139
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Lazy Prisma client singleton for apps/mcp's `CONTENT_ADAPTER=database` path
3
+ * (#5876 spec D7).
4
+ *
5
+ * Deliberately NOT the eager, module-scope-construction shape of
6
+ * `apps/api/src/lib/prisma.ts` / `apps/web/lib/prisma-for-auth.ts`: this
7
+ * module is reached only from `initMcpPublicWorkspace()`'s `database` branch
8
+ * (`public-workspace.ts`), via `await import("./prisma.js")`, and only when
9
+ * the caller supplied no `prisma` of its own — the injection seam every test
10
+ * uses instead (FR-1). Constructing at MODULE scope — with a
11
+ * `connectionString` that is `undefined` on every MDX-only deployment (the
12
+ * 350 pre-existing apps/mcp tests set no `DATABASE_URL`) — would break the
13
+ * no-`DATABASE_URL` scaffold the instant this file was imported anywhere,
14
+ * even transitively. Construction happens on first CALL and is memoised
15
+ * after that, mirroring #5852's identical deferral of the apps/api client
16
+ * (`apps/api/src/services/public-site-resolver.ts` § D6).
17
+ */
18
+ import { PrismaPg } from "@prisma/adapter-pg";
19
+ import { PrismaClient } from "@working-theory/database";
20
+
21
+ let client: PrismaClient | undefined;
22
+
23
+ /**
24
+ * The process-wide lazy Prisma client for the MCP server's database path.
25
+ * Constructs on first call; every subsequent call returns the same instance.
26
+ */
27
+ export function getMcpPrismaClient(): PrismaClient {
28
+ if (client === undefined) {
29
+ const adapter = new PrismaPg({
30
+ connectionString: process.env.DATABASE_URL,
31
+ });
32
+ client = new PrismaClient({
33
+ adapter,
34
+ log:
35
+ process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
36
+ });
37
+ }
38
+ return client;
39
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The MCP server's own workspace resolution (#5876 spec D2/D4/D6/D7) — which
3
+ * workspace `CONTENT_ADAPTER=database` content reads serve, resolved ONCE at
4
+ * bootstrap and read synchronously thereafter by `content-adapter.ts`.
5
+ *
6
+ * WHY RESOLUTION HAPPENS AT BOOTSTRAP, NOT PER CALL (D4). Workspace
7
+ * resolution is I/O (a `workspace.count()` + `findUnique()`), but
8
+ * `getMcpContentAdapter()` stays SYNCHRONOUS — every one of its thirteen
9
+ * call sites (nine read tools, `audit-single-page.ts`, and the three write
10
+ * tools) calls it without `await`. Making it async would force `await` at
11
+ * every one of those sites, a needless cross-cutting edit to a hard-floor
12
+ * surface (`apps/mcp/src/tools/**`). So `apps/mcp/src/index.ts` awaits
13
+ * `initMcpPublicWorkspace()` once, before either transport branch, and the
14
+ * result is memoised here for `mcpPublicWorkspace()`'s synchronous read.
15
+ *
16
+ * A tool invoked before `initMcpPublicWorkspace()` has resolved sees the
17
+ * module's initial `{ kind: "not-required" }` state — which the seam reads
18
+ * exactly like an unresolved deployment under `database` mode (the empty
19
+ * adapter, never content). That is the accepted, safe-direction cost D4
20
+ * names, and it's what keeps the 350 pre-existing tests — which never call
21
+ * `init` — on the byte-identical MDX path.
22
+ *
23
+ * NO-OP UNLESS THE ENVIRONMENT SELECTS `database` (D6). On `mdx` and
24
+ * `projection`, `initMcpPublicWorkspace()`'s first statement is the same
25
+ * `resolvedAdapterKind()` test the seam itself uses, and it returns before
26
+ * any `await import()` — no database round-trip, no `DATABASE_URL` read, on
27
+ * every MDX-only deployment.
28
+ *
29
+ * THE ONE-WORKSPACE PREDICATE IS SHARED, NOT REIMPLEMENTED (D2). This module
30
+ * issues no direct tenant-model query of its own — it delegates to
31
+ * `@working-theory/database/tenancy`'s `resolveDefaultWorkspaceRow`, the same
32
+ * predicate `apps/api/src/services/default-tenant.ts` re-points at. One
33
+ * predicate, reached from two apps, never two predicates (NFR-7).
34
+ *
35
+ * THE LAZY CLIENT (D7). The `prisma` injection option is the seam AC-1/AC-2
36
+ * are scored through: when the caller supplies one, `./prisma.js` (the
37
+ * module that constructs a real `PrismaClient`) is never imported — which is
38
+ * what keeps every test in this repo's ordinary Jest run off a real
39
+ * database connection (NFR-6).
40
+ */
41
+ import {
42
+ resolvedAdapterKind,
43
+ type ArtifactPrismaClient,
44
+ } from "@working-theory/content-adapters";
45
+ import {
46
+ resolveDefaultWorkspaceRow,
47
+ type WorkspacePredicateClient,
48
+ } from "@working-theory/database/tenancy";
49
+
50
+ /**
51
+ * The three answers the MCP server's own workspace resolution can reach.
52
+ * `reason` exists for the caller's own logging only — no MCP tool emits it
53
+ * to the (anonymous, unauthenticated) caller (NFR-4, mirroring
54
+ * `apps/api/src/services/public-site-resolver.ts`'s identical
55
+ * `PublicSiteResolution`). Unlike that HTTP twin, there is no `host-unmatched`
56
+ * arm here: an MCP stdio session carries no HTTP request and no host to
57
+ * mismatch against (§2.3 — a host→workspace branch is explicitly out of
58
+ * scope, #3635's to add).
59
+ */
60
+ export type McpWorkspaceResolution =
61
+ | { kind: "not-required" }
62
+ | { kind: "resolved"; workspaceId: string; prisma: ArtifactPrismaClient }
63
+ | { kind: "unresolved"; reason: "ambiguous" };
64
+
65
+ export interface InitMcpPublicWorkspaceOptions {
66
+ /**
67
+ * Injection seam for tests (D7, D11) — when supplied, `initMcpPublicWorkspace()`
68
+ * uses it and never loads `./prisma.js`. Typed as the narrow surface THIS
69
+ * function itself calls (`workspace.count`/`findUnique`); the real
70
+ * `PrismaClient` (or a caller-injected structural equivalent) is
71
+ * structurally wider and also satisfies `ArtifactPrismaClient`, which is
72
+ * what the seam later forwards to `resolveContentAdapter()`.
73
+ */
74
+ prisma?: WorkspacePredicateClient;
75
+ }
76
+
77
+ let resolution: McpWorkspaceResolution = { kind: "not-required" };
78
+
79
+ /**
80
+ * Resolves the MCP server's workspace, once. Memoises the result for
81
+ * `mcpPublicWorkspace()`'s synchronous read. Safe to call more than once
82
+ * (each call re-resolves and re-memoises) — production calls it exactly once,
83
+ * at bootstrap (`index.ts`).
84
+ */
85
+ export async function initMcpPublicWorkspace(
86
+ options: InitMcpPublicWorkspaceOptions = {},
87
+ ): Promise<McpWorkspaceResolution> {
88
+ if (resolvedAdapterKind() !== "database") {
89
+ resolution = { kind: "not-required" };
90
+ return resolution;
91
+ }
92
+
93
+ // Deferred — see this file's header (D7) and #5852's identical deferral of
94
+ // apps/api's own client (`services/public-site-resolver.ts`).
95
+ const prisma =
96
+ options.prisma ?? (await import("./prisma.js")).getMcpPrismaClient();
97
+ const workspace = await resolveDefaultWorkspaceRow(prisma);
98
+ resolution =
99
+ workspace === null
100
+ ? { kind: "unresolved", reason: "ambiguous" }
101
+ : {
102
+ kind: "resolved",
103
+ workspaceId: workspace.id,
104
+ prisma: prisma as unknown as ArtifactPrismaClient,
105
+ };
106
+ return resolution;
107
+ }
108
+
109
+ /**
110
+ * The memoised resolution, read synchronously. `content-adapter.ts`'s
111
+ * `database` branch is the one caller in production; every arm other than
112
+ * `{ kind: "resolved" }` maps to the fail-closed empty adapter.
113
+ */
114
+ export function mcpPublicWorkspace(): McpWorkspaceResolution {
115
+ return resolution;
116
+ }
117
+
118
+ /** Test-only injector — sets the memoised resolution directly, bypassing the
119
+ * tenancy predicate entirely. This is the seam `read_content.test.ts`,
120
+ * `list_pages.test.ts` and `fail-closed-tools.test.ts` use: they care about
121
+ * the CONTENT tools' behaviour under a given resolution, not about exercising
122
+ * the resolution logic itself (that's `public-workspace.test.ts`'s job, via
123
+ * the real `initMcpPublicWorkspace({ prisma: <stub> })`). */
124
+ export function __setMcpPublicWorkspaceForTest(
125
+ next: McpWorkspaceResolution,
126
+ ): void {
127
+ resolution = next;
128
+ }
129
+
130
+ /** Test-only reset — returns to the module's initial, pre-bootstrap state. */
131
+ export function resetMcpPublicWorkspace(): void {
132
+ resolution = { kind: "not-required" };
133
+ }