create-ailk 0.4.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 (396) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +12248 -0
  3. package/component-catalog.md +193 -68
  4. package/dist/cli.js +29 -1
  5. package/dist/derive-architecture.js +5 -2
  6. package/dist/lib/api-boot-assertion.d.ts +46 -0
  7. package/dist/lib/api-boot-assertion.js +70 -0
  8. package/dist/lib/extract-module-bundle.d.ts +50 -2
  9. package/dist/lib/extract-module-bundle.js +223 -15
  10. package/dist/lib/fetch-guard.d.ts +28 -0
  11. package/dist/lib/fetch-guard.js +101 -0
  12. package/dist/lib/fetch-module.js +4 -0
  13. package/dist/lib/fetch-template.js +4 -0
  14. package/dist/lib/import-graph.d.ts +29 -0
  15. package/dist/lib/import-graph.js +140 -0
  16. package/dist/module-architecture.d.ts +60 -8
  17. package/dist/module-architecture.js +587 -242
  18. package/dist/parse-args.js +6 -2
  19. package/dist/programmatic.d.ts +10 -0
  20. package/dist/programmatic.js +134 -19
  21. package/dist/site-chrome.d.ts +80 -0
  22. package/dist/site-chrome.js +135 -0
  23. package/dist/surfaces.d.ts +21 -6
  24. package/dist/surfaces.js +209 -36
  25. package/dist/template-manifest.d.ts +88 -3
  26. package/dist/template-manifest.js +281 -29
  27. package/dist/template-names.d.ts +1 -1
  28. package/dist/template-names.js +2 -0
  29. package/package.json +10 -2
  30. package/site-templates/_base/ailk-template.json +6 -0
  31. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
  34. package/site-templates/marketing-site/ailk-template.json +11 -0
  35. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  36. package/site-templates/marketing-site/provenance.json +4 -0
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
  39. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
  41. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
  42. package/site-templates/minimalism-site/ailk-template.json +22 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  44. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  49. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  50. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  51. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  53. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  54. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  55. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  56. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  57. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
  58. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  60. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  65. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  68. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  69. package/site-templates/minimalism-site/provenance.json +4 -0
  70. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  71. package/site-templates/saas-product/ailk-template.json +16 -0
  72. package/site-templates/saas-product/provenance.json +10 -0
  73. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  74. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  75. package/site-templates/simple-service/ailk-template.json +10 -0
  76. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  77. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  78. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  79. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  80. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  81. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  82. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  83. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  84. package/site-templates/simple-service/provenance.json +4 -0
  85. package/templates/.claude/skills/README.md +16 -25
  86. package/templates/.env.example +102 -12
  87. package/templates/.github/workflows/ci.yml +48 -0
  88. package/templates/README.md +3 -1
  89. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  90. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  91. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  92. package/templates/apps/api/.env.example +13 -0
  93. package/templates/apps/api/CLAUDE.md +29 -2
  94. package/templates/apps/api/eslint.config.mjs +69 -0
  95. package/templates/apps/api/jest.config.cjs +3 -0
  96. package/templates/apps/api/package.json +7 -2
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  98. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
  99. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  100. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  101. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  102. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
  103. package/templates/apps/api/src/config/index.ts +62 -0
  104. package/templates/apps/api/src/config/modules.ts +7 -3
  105. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
  106. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  107. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  108. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  109. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  110. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  111. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  112. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  113. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  114. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  115. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  116. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  117. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  118. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  119. package/templates/apps/api/src/lib/site.ts +208 -9
  120. package/templates/apps/api/src/lib/stripe.ts +66 -0
  121. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
  122. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
  123. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  124. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  125. package/templates/apps/api/src/openapi/spec.ts +543 -2
  126. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  127. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  128. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  129. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  130. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  131. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  132. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  133. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  134. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  135. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  136. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  137. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  138. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  139. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  140. package/templates/apps/api/src/routes/content/index.ts +13 -73
  141. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  142. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  143. package/templates/apps/api/src/routes/content/search.ts +129 -0
  144. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  145. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  146. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  147. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  148. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  149. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  150. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  151. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  152. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  153. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  154. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  155. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  156. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  157. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  158. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  159. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  160. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  161. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  162. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  163. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  164. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  165. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  166. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  167. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  168. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  169. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  170. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  171. package/templates/apps/api/src/server.ts +521 -55
  172. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  173. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  174. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  175. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  176. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  177. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  178. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  179. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  180. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  181. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  182. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  183. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  184. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  185. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  186. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  187. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  188. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  189. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  190. package/templates/apps/api/src/services/audit-log.ts +668 -0
  191. package/templates/apps/api/src/services/consent.ts +18 -0
  192. package/templates/apps/api/src/services/default-tenant.ts +300 -0
  193. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  194. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  195. package/templates/apps/api/src/services/entitlements.ts +80 -0
  196. package/templates/apps/api/src/services/lead-promotion.ts +160 -0
  197. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  198. package/templates/apps/api/src/services/leads-inbox.ts +284 -0
  199. package/templates/apps/api/src/services/listing-config.ts +27 -9
  200. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  201. package/templates/apps/api/src/services/project-listings.ts +201 -6
  202. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  203. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  204. package/templates/apps/api/src/services/revalidation.ts +173 -0
  205. package/templates/apps/api/src/services/verified-email.ts +71 -0
  206. package/templates/apps/api/src/vercel-handler.ts +75 -35
  207. package/templates/apps/mcp/.env.example +17 -0
  208. package/templates/apps/mcp/CLAUDE.md +58 -25
  209. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  210. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  211. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  212. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  213. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  214. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  215. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  216. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  217. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  218. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  219. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  220. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  221. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  222. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  223. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  224. package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
  225. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  226. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  227. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  228. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  229. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  230. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  231. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  232. package/templates/apps/mcp/eslint.config.mjs +69 -0
  233. package/templates/apps/mcp/jest.config.cjs +13 -0
  234. package/templates/apps/mcp/package.json +8 -2
  235. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  236. package/templates/apps/mcp/src/config/modules.ts +10 -5
  237. package/templates/apps/mcp/src/http-transport.ts +102 -29
  238. package/templates/apps/mcp/src/index.ts +73 -11
  239. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  240. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  241. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  242. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  243. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  244. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  245. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  246. package/templates/apps/mcp/src/server.ts +61 -37
  247. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  248. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  249. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  250. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  251. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  252. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  253. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  254. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  255. package/templates/apps/mcp/src/tools/index.ts +9 -7
  256. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  257. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  258. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  259. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  260. package/templates/apps/web/.env.example +74 -8
  261. package/templates/apps/web/CLAUDE.md +46 -0
  262. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  263. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  264. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  265. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  266. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  267. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  268. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  269. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  270. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  271. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  272. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  273. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  274. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  275. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  276. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  277. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  278. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  279. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  280. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  281. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
  282. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
  283. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  284. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  285. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  286. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  287. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  288. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  289. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  290. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  291. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  292. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  293. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  294. package/templates/apps/web/app/[locale]/layout.tsx +308 -65
  295. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  296. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  297. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  298. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  299. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  300. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  301. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  302. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  303. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  304. package/templates/apps/web/auth.ts +407 -8
  305. package/templates/apps/web/components/google-analytics.tsx +18 -1
  306. package/templates/apps/web/eslint.config.mjs +69 -0
  307. package/templates/apps/web/jest.config.cjs +15 -19
  308. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  309. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  310. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  311. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
  312. package/templates/apps/web/lib/auth-email.ts +318 -0
  313. package/templates/apps/web/lib/auth-env.ts +85 -0
  314. package/templates/apps/web/lib/auth-routes.ts +55 -0
  315. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  316. package/templates/apps/web/lib/nav-config.ts +27 -7
  317. package/templates/apps/web/lib/server-api.ts +1 -1
  318. package/templates/apps/web/lib/site-chrome.ts +29 -0
  319. package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
  320. package/templates/apps/web/messages/en.json +216 -2
  321. package/templates/apps/web/next.config.mjs +20 -0
  322. package/templates/apps/web/package.json +8 -3
  323. package/templates/apps/web/sentry.client.config.ts +131 -0
  324. package/templates/apps/web/sentry.edge.config.ts +72 -0
  325. package/templates/apps/web/sentry.server.config.ts +72 -0
  326. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  327. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  328. package/templates/apps/web/src/lib/routes.ts +13 -18
  329. package/templates/brand_assets/README.md +24 -0
  330. package/templates/content/en/AboutPage/about.mdx +160 -15
  331. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  332. package/templates/content/en/ContactPage/contact.mdx +90 -13
  333. package/templates/content/en/FAQPage/faq.mdx +62 -22
  334. package/templates/content/en/HomePage/home.mdx +244 -20
  335. package/templates/content/en/LandingPage/blog.mdx +83 -0
  336. package/templates/content/en/LandingPage/features.mdx +103 -0
  337. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  338. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  339. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  340. package/templates/database/CHANGELOG.md +149 -0
  341. package/templates/database/README.md +10 -3
  342. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  343. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  344. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  345. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  346. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  347. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  348. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  349. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  350. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  351. package/templates/database/__tests__/seed.test.ts +1 -1
  352. package/templates/database/__tests__/tenancy.test.ts +141 -0
  353. package/templates/database/config/schema.prisma +79 -2
  354. package/templates/database/content/schema.prisma +87 -11
  355. package/templates/database/eslint.config.mjs +69 -0
  356. package/templates/database/inbox/schema.prisma +452 -9
  357. package/templates/database/jest.config.cjs +5 -3
  358. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  359. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  360. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  361. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  362. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  363. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  364. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  365. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  366. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  367. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  368. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  369. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  370. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  371. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  372. package/templates/database/package.json +7 -3
  373. package/templates/database/seed.ts +1 -1
  374. package/templates/database/tenancy.ts +86 -1
  375. package/templates/database/tsconfig.json +1 -0
  376. package/templates/package.json +1 -14
  377. package/versions.json +52 -0
  378. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  379. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  380. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  381. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  382. package/templates/apps/api/src/routes/content/create.ts +0 -115
  383. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  384. package/templates/apps/api/src/routes/content/update.ts +0 -112
  385. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  386. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  387. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  388. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  389. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  390. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  391. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  392. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  393. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  394. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  395. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
  396. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Tests for `scanSitePages` (apps/mcp/src/lib/audit-single-page.ts), exercised
3
+ * through `buildSinglePageAuditInput` with a mocked `ContentAdapter` (#6012,
4
+ * a follow-up from the #6010 review).
5
+ *
6
+ * `scanSitePages` treats two conditions differently, and this file is the
7
+ * test that proves it:
8
+ *
9
+ * - a type the adapter has no pages of resolves NORMALLY to `[]` — never an
10
+ * error, never logged. This is the ordinary "site has nothing of this
11
+ * type" case every real adapter (MDX, database) takes today.
12
+ * - a type whose adapter call THROWS is a genuine failure. It must not take
13
+ * down the audit (the other four reads still complete and their pages
14
+ * still count), but it must be visible — logged via `console.error`
15
+ * (stderr, NOT stdout — the default stdio MCP transport's JSON-RPC
16
+ * framing channel, #6012 round 2) — rather than silently collapsing into
17
+ * the same vacuous "nothing found" result.
18
+ *
19
+ * The call-count assertions below prove that all five reads still ran despite
20
+ * the throw — the read AFTER the failing one is not skipped, which a
21
+ * sequential loop that ABORTED on the first throw would fail to show. They
22
+ * are NOT, on their own, proof of CONCURRENCY: the prior sequential `for`
23
+ * loop this replaced used `continue` (not a re-throw), so it too issued all
24
+ * five calls. The `Promise.all` in the source is what delivers the
25
+ * concurrency AC; these assertions cover the narrower, still load-bearing
26
+ * property that a throw part-way through never truncates the scan.
27
+ */
28
+
29
+ const mockResolveContentAdapter = jest.fn();
30
+
31
+ jest.mock("@working-theory/content-adapters", () => ({
32
+ ...jest.requireActual("@working-theory/content-adapters"),
33
+ resolveContentAdapter: (...args: unknown[]) =>
34
+ mockResolveContentAdapter(...args),
35
+ }));
36
+
37
+ import type { ContentPage } from "@working-theory/content-adapters";
38
+
39
+ import { buildSinglePageAuditInput } from "../../src/lib/audit-single-page.js";
40
+
41
+ const HOME_PAGE: ContentPage = {
42
+ type: "HomePage",
43
+ slug: "home",
44
+ title: "Home",
45
+ description: "The home page.",
46
+ };
47
+
48
+ const PRODUCT_PAGE: ContentPage = {
49
+ type: "Product",
50
+ slug: "widget",
51
+ title: "Widget",
52
+ description: "A widget for sale.",
53
+ price: 49,
54
+ priceCurrency: "USD",
55
+ };
56
+
57
+ describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () => {
58
+ const previous = process.env.CONTENT_ADAPTER;
59
+ let errorSpy: jest.SpyInstance;
60
+
61
+ beforeEach(() => {
62
+ mockResolveContentAdapter.mockReset();
63
+ process.env.CONTENT_ADAPTER = "mdx";
64
+ errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
65
+ });
66
+
67
+ afterEach(() => {
68
+ if (previous === undefined) delete process.env.CONTENT_ADAPTER;
69
+ else process.env.CONTENT_ADAPTER = previous;
70
+ errorSpy.mockRestore();
71
+ });
72
+
73
+ it("a type the adapter legitimately has no pages of resolves silently — nothing logged", async () => {
74
+ const listPages = jest.fn().mockResolvedValue([]);
75
+ const listDetail = jest.fn().mockResolvedValue([]);
76
+ mockResolveContentAdapter.mockReturnValue({
77
+ getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
78
+ listPages,
79
+ listDetail,
80
+ getDetailBySlug: jest.fn(),
81
+ searchPages: jest.fn(),
82
+ extractStructuredData: jest.fn(),
83
+ createPage: jest.fn(),
84
+ updatePage: jest.fn(),
85
+ deletePage: jest.fn(),
86
+ getPageVersion: jest.fn(),
87
+ });
88
+
89
+ const result = await buildSinglePageAuditInput({
90
+ slug: "home",
91
+ type: "HomePage",
92
+ });
93
+
94
+ expect(result.readiness!.sitePages).toEqual([]);
95
+ expect(errorSpy).not.toHaveBeenCalled();
96
+ // All five reads still ran — two listDetail (Product, Service), three
97
+ // listPages (the three policy types).
98
+ expect(listDetail).toHaveBeenCalledTimes(2);
99
+ expect(listPages).toHaveBeenCalledTimes(3);
100
+ });
101
+
102
+ it("a type whose adapter call throws is logged to stderr and does not fail the audit", async () => {
103
+ const listPages = jest.fn().mockResolvedValue([]);
104
+ const listDetail = jest.fn().mockImplementation((type: string) => {
105
+ if (type === "Service") {
106
+ throw new Error("simulated adapter failure");
107
+ }
108
+ return Promise.resolve([PRODUCT_PAGE]);
109
+ });
110
+ mockResolveContentAdapter.mockReturnValue({
111
+ getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
112
+ listPages,
113
+ listDetail,
114
+ getDetailBySlug: jest.fn(),
115
+ searchPages: jest.fn(),
116
+ extractStructuredData: jest.fn(),
117
+ createPage: jest.fn(),
118
+ updatePage: jest.fn(),
119
+ deletePage: jest.fn(),
120
+ getPageVersion: jest.fn(),
121
+ });
122
+
123
+ const result = await buildSinglePageAuditInput({
124
+ slug: "home",
125
+ type: "HomePage",
126
+ });
127
+
128
+ // The audit as a whole never fails, and the type that threw contributes
129
+ // nothing — but the type that succeeded (Product) still counts.
130
+ expect(result.readiness!.sitePages).toEqual([
131
+ { slug: "widget", type: "Product", price: 49 },
132
+ ]);
133
+ // The throwing type is visible, not silently swallowed — and via
134
+ // console.error (stderr), never console.debug/console.log (stdout,
135
+ // the stdio transport's JSON-RPC channel).
136
+ expect(errorSpy).toHaveBeenCalledTimes(1);
137
+ expect(errorSpy.mock.calls[0]![0]).toMatch(/Service/);
138
+ // All five reads still ran despite the throw — the read AFTER the
139
+ // throwing one (ReturnsPolicyPage, et al.) was not skipped.
140
+ expect(listDetail).toHaveBeenCalledTimes(2);
141
+ expect(listPages).toHaveBeenCalledTimes(3);
142
+ });
143
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Tests for the boot-memoised commerce claim (#5971 spec D2).
3
+ *
4
+ * Idiom: `__tests__/lib/require-paid-tier.test.ts` — `checkLicense()` is
5
+ * injected, never called for real, so no suite reaches a licence server
6
+ * (NFR-6). Every arm that cannot POSITIVELY establish the claim must resolve
7
+ * to `"unclaimed"` (NFR-3, fail closed).
8
+ */
9
+
10
+ import {
11
+ initMcpCommerceClaim,
12
+ mcpCommerceClaim,
13
+ resetMcpCommerceClaim,
14
+ __setMcpCommerceClaimForTest,
15
+ } from "../../src/lib/commerce-claim.js";
16
+
17
+ /**
18
+ * The injected licence reader. Typed off `initMcpCommerceClaim`'s own option
19
+ * rather than off `@working-theory/license`, so the stubs below satisfy
20
+ * exactly what the function under test calls — no double assertion needed.
21
+ */
22
+ type CheckLicenseSeam = NonNullable<
23
+ NonNullable<Parameters<typeof initMcpCommerceClaim>[0]>["checkLicense"]
24
+ >;
25
+
26
+ type LicenseShape = Awaited<ReturnType<CheckLicenseSeam>>;
27
+
28
+ /** A licence reader that resolves to the given result. */
29
+ function resolving(result: Partial<LicenseShape>): CheckLicenseSeam {
30
+ return () => Promise.resolve(result as LicenseShape);
31
+ }
32
+
33
+ /** A licence reader that rejects — the "an exception is a bug" arm. */
34
+ function rejecting(): CheckLicenseSeam {
35
+ return () => Promise.reject(new Error("boom"));
36
+ }
37
+
38
+ afterEach(() => {
39
+ resetMcpCommerceClaim();
40
+ });
41
+
42
+ describe("initMcpCommerceClaim", () => {
43
+ it("resolves claimed when features[] carries 'commerce'", async () => {
44
+ const claim = await initMcpCommerceClaim({
45
+ checkLicense: resolving({ tier: "pro", features: ["commerce"] }),
46
+ });
47
+
48
+ expect(claim).toBe("claimed");
49
+ expect(mcpCommerceClaim()).toBe("claimed");
50
+ });
51
+
52
+ it("resolves unclaimed when features[] omits 'commerce'", async () => {
53
+ await initMcpCommerceClaim({
54
+ checkLicense: resolving({ tier: "pro", features: ["scheduling"] }),
55
+ });
56
+
57
+ expect(mcpCommerceClaim()).toBe("unclaimed");
58
+ });
59
+
60
+ it("resolves unclaimed on a free tier with no features", async () => {
61
+ await initMcpCommerceClaim({
62
+ checkLicense: resolving({ tier: "free", features: [] }),
63
+ });
64
+
65
+ expect(mcpCommerceClaim()).toBe("unclaimed");
66
+ });
67
+
68
+ it("resolves unclaimed when the licence is revoked, even carrying the feature", async () => {
69
+ await initMcpCommerceClaim({
70
+ checkLicense: resolving({
71
+ tier: "pro",
72
+ features: ["commerce"],
73
+ revoked: true,
74
+ }),
75
+ });
76
+
77
+ expect(mcpCommerceClaim()).toBe("unclaimed");
78
+ });
79
+
80
+ it("resolves unclaimed — never throws — when checkLicense() rejects", async () => {
81
+ const claim = await initMcpCommerceClaim({
82
+ checkLicense: rejecting(),
83
+ });
84
+
85
+ expect(claim).toBe("unclaimed");
86
+ expect(mcpCommerceClaim()).toBe("unclaimed");
87
+ });
88
+
89
+ it("re-resolves and re-memoises on a second call", async () => {
90
+ await initMcpCommerceClaim({
91
+ checkLicense: resolving({ tier: "pro", features: ["commerce"] }),
92
+ });
93
+ expect(mcpCommerceClaim()).toBe("claimed");
94
+
95
+ await initMcpCommerceClaim({
96
+ checkLicense: resolving({ tier: "free", features: [] }),
97
+ });
98
+ expect(mcpCommerceClaim()).toBe("unclaimed");
99
+ });
100
+ });
101
+
102
+ describe("mcpCommerceClaim — fail-closed initial state", () => {
103
+ it("is unclaimed before any init", () => {
104
+ expect(mcpCommerceClaim()).toBe("unclaimed");
105
+ });
106
+
107
+ it("the test injector sets it directly", () => {
108
+ __setMcpCommerceClaimForTest("claimed");
109
+ expect(mcpCommerceClaim()).toBe("claimed");
110
+ });
111
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * MCP ↔ API content parity through the one resolver (#5805, Epic #5322 C1).
3
+ *
4
+ * `apps/api` resolves its adapter with `resolveContentAdapter()`
5
+ * (`apps/api/src/lib/content-adapter.ts`); after this change `apps/mcp` does
6
+ * too, via `getMcpContentAdapter()`. This test proves the two consumers read
7
+ * the same page for the same slug when pointed at the same content root — the
8
+ * whole point of one-function resolution.
9
+ *
10
+ * It resolves the "API side" by calling `resolveContentAdapter()` directly
11
+ * with the shape `apps/api` passes, rather than importing `apps/api` — an
12
+ * app-to-app import is banned by the architecture boundary. The assertion is
13
+ * deliberately small: this is a proof of one-function resolution, not a second
14
+ * content-contract suite.
15
+ */
16
+
17
+ import { join, resolve } from "node:path";
18
+
19
+ import {
20
+ resolveContentAdapter,
21
+ resetResolvedContentAdapter,
22
+ } from "@working-theory/content-adapters";
23
+
24
+ import { resolveLocale } from "../../src/lib/resolve-locale.js";
25
+ import { ailkReadContent } from "../../src/tools/read_content.js";
26
+
27
+ const REPO_ROOT = resolve(__dirname, "../../../..");
28
+ const CONTENT_ROOT = join(REPO_ROOT, "content");
29
+
30
+ describe("read_content ↔ apps/api adapter parity (CONTENT_ADAPTER=mdx)", () => {
31
+ const previous = process.env.CONTENT_ADAPTER;
32
+
33
+ beforeEach(() => {
34
+ process.env.CONTENT_ADAPTER = "mdx";
35
+ resetResolvedContentAdapter();
36
+ });
37
+
38
+ afterEach(() => {
39
+ if (previous === undefined) delete process.env.CONTENT_ADAPTER;
40
+ else process.env.CONTENT_ADAPTER = previous;
41
+ resetResolvedContentAdapter();
42
+ });
43
+
44
+ it("returns the same title and slug the API's adapter returns", async () => {
45
+ const viaMcp = await ailkReadContent(
46
+ { type: "HomePage", slug: "home" },
47
+ CONTENT_ROOT,
48
+ );
49
+
50
+ // The shape apps/api hands the resolver: a content root and the project's
51
+ // default locale, no workspaceId. Both sides read `project.yaml` for that
52
+ // locale, so resolving it once here keeps the two calls comparable.
53
+ const locale = resolveLocale(undefined);
54
+ const apiAdapter = resolveContentAdapter({
55
+ contentRoot: CONTENT_ROOT,
56
+ defaultLocale: locale,
57
+ });
58
+ const viaApi = await apiAdapter.getPageBySlug("HomePage", "home", locale);
59
+
60
+ expect(viaMcp).not.toBeNull();
61
+ expect(viaApi).not.toBeNull();
62
+ expect(viaMcp!.slug).toBe(viaApi!.slug);
63
+ expect(viaMcp!.frontmatter.title).toBe(viaApi!.title);
64
+ });
65
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Tests for the shared MCP content-adapter helper (#5805, extended #5876).
3
+ *
4
+ * Epic #5322 exit criterion 2: every content consumer resolves its adapter
5
+ * through one function. These tests pin the delegation contract of
6
+ * `getMcpContentAdapter()` — the single seam the twelve MCP tools and
7
+ * `audit-single-page` now go through:
8
+ *
9
+ * - it delegates to `resolveContentAdapter()` rather than constructing an
10
+ * adapter itself;
11
+ * - it forwards the caller's `contentRoot` test-override verbatim;
12
+ * - it forwards the caller's resolved `locale` as `defaultLocale`;
13
+ * - it defaults `contentRoot` to `<cwd>/content` when the caller passes none;
14
+ * - its MDX-narrowed twin `getMcpMdxContentAdapter()` (which `read_content`
15
+ * needs for `getPageSource()`) resolves the same way and raises a named
16
+ * error rather than a TypeError when the selected adapter is not MDX;
17
+ * - on `CONTENT_ADAPTER=mdx` (the default, unset), it passes NO `workspaceId`;
18
+ * - on `CONTENT_ADAPTER=database`, it reads `mcpPublicWorkspace()` (#5876
19
+ * spec D2/D4/D5/D6): a resolved workspace forwards `workspaceId` + `prisma`
20
+ * to `resolveContentAdapter()`; an unresolved one calls the resolver NOT AT
21
+ * ALL and returns the fail-closed empty adapter instead.
22
+ */
23
+
24
+ import { join } from "node:path";
25
+
26
+ const mockResolveContentAdapter = jest.fn();
27
+
28
+ jest.mock("@working-theory/content-adapters", () => ({
29
+ ...jest.requireActual("@working-theory/content-adapters"),
30
+ resolveContentAdapter: (...args: unknown[]) =>
31
+ mockResolveContentAdapter(...args),
32
+ }));
33
+
34
+ import type { ArtifactPrismaClient } from "@working-theory/content-adapters";
35
+
36
+ import {
37
+ getMcpContentAdapter,
38
+ getMcpMdxContentAdapter,
39
+ } from "../../src/lib/content-adapter.js";
40
+ import {
41
+ __setMcpPublicWorkspaceForTest,
42
+ resetMcpPublicWorkspace,
43
+ } from "../../src/lib/public-workspace.js";
44
+
45
+ describe("getMcpContentAdapter", () => {
46
+ const previous = process.env.CONTENT_ADAPTER;
47
+
48
+ beforeEach(() => {
49
+ mockResolveContentAdapter.mockReset();
50
+ mockResolveContentAdapter.mockReturnValue({ marker: "resolved" });
51
+ process.env.CONTENT_ADAPTER = "mdx";
52
+ });
53
+
54
+ afterEach(() => {
55
+ if (previous === undefined) delete process.env.CONTENT_ADAPTER;
56
+ else process.env.CONTENT_ADAPTER = previous;
57
+ });
58
+
59
+ it("delegates to resolveContentAdapter and returns what it builds", () => {
60
+ const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
61
+
62
+ expect(mockResolveContentAdapter).toHaveBeenCalledTimes(1);
63
+ expect(adapter).toEqual({ marker: "resolved" });
64
+ });
65
+
66
+ it("forwards contentRoot and locale to the resolver", () => {
67
+ getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "fr-CA" });
68
+
69
+ expect(mockResolveContentAdapter).toHaveBeenCalledWith({
70
+ contentRoot: "/fixtures/content",
71
+ defaultLocale: "fr-CA",
72
+ });
73
+ });
74
+
75
+ it("defaults contentRoot to <cwd>/content when none is given", () => {
76
+ getMcpContentAdapter();
77
+
78
+ expect(mockResolveContentAdapter).toHaveBeenCalledWith({
79
+ contentRoot: join(process.cwd(), "content"),
80
+ defaultLocale: undefined,
81
+ });
82
+ });
83
+
84
+ it("passes no workspaceId on CONTENT_ADAPTER=mdx — the MDX path stays byte-identical", () => {
85
+ getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "en-US" });
86
+
87
+ const options = mockResolveContentAdapter.mock.calls[0]![0] as Record<
88
+ string,
89
+ unknown
90
+ >;
91
+ expect(options).not.toHaveProperty("workspaceId");
92
+ });
93
+ });
94
+
95
+ describe("getMcpContentAdapter — CONTENT_ADAPTER=database", () => {
96
+ const previous = process.env.CONTENT_ADAPTER;
97
+
98
+ beforeEach(() => {
99
+ mockResolveContentAdapter.mockReset();
100
+ // A full MdxContentAdapter-shaped stub — getMcpContentAdapter()'s database
101
+ // branch always passes the resolver's return through readOnlyContentAdapter
102
+ // (content-adapter.ts), which binds every read method.
103
+ mockResolveContentAdapter.mockReturnValue({
104
+ getPageBySlug: jest.fn(),
105
+ listPages: jest.fn(),
106
+ getDetailBySlug: jest.fn(),
107
+ listDetail: jest.fn(),
108
+ searchPages: jest.fn(),
109
+ extractStructuredData: jest.fn(),
110
+ getPageSource: jest.fn(),
111
+ createPage: jest.fn(),
112
+ updatePage: jest.fn(),
113
+ deletePage: jest.fn(),
114
+ getPageVersion: jest.fn(),
115
+ });
116
+ process.env.CONTENT_ADAPTER = "database";
117
+ });
118
+
119
+ afterEach(() => {
120
+ if (previous === undefined) delete process.env.CONTENT_ADAPTER;
121
+ else process.env.CONTENT_ADAPTER = previous;
122
+ resetMcpPublicWorkspace();
123
+ });
124
+
125
+ it("forwards workspaceId and prisma to the resolver when the workspace resolved", () => {
126
+ // This test only asserts the prisma VALUE is forwarded by reference — it
127
+ // never calls a method on it, so a minimal marker object stands in.
128
+ // eslint-disable-next-line no-restricted-syntax -- structural test stub, mirrors audit-single-page.test.ts's identical pattern
129
+ const prismaStub = {
130
+ marker: "prisma-stub",
131
+ } as unknown as ArtifactPrismaClient;
132
+ __setMcpPublicWorkspaceForTest({
133
+ kind: "resolved",
134
+ workspaceId: "wksp_00000000-0000-7000-8000-000000000001",
135
+ prisma: prismaStub,
136
+ });
137
+
138
+ getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "en-US" });
139
+
140
+ expect(mockResolveContentAdapter).toHaveBeenCalledWith({
141
+ workspaceId: "wksp_00000000-0000-7000-8000-000000000001",
142
+ prisma: prismaStub,
143
+ contentRoot: "/fixtures/content",
144
+ defaultLocale: "en-US",
145
+ });
146
+ });
147
+
148
+ it("never calls the resolver and returns the fail-closed empty adapter when unresolved", async () => {
149
+ __setMcpPublicWorkspaceForTest({ kind: "unresolved", reason: "ambiguous" });
150
+
151
+ const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
152
+
153
+ expect(mockResolveContentAdapter).not.toHaveBeenCalled();
154
+ await expect(adapter.getPageBySlug("HomePage", "home")).resolves.toBeNull();
155
+ await expect(adapter.listPages("HomePage")).resolves.toEqual([]);
156
+ });
157
+
158
+ it("never calls the resolver and returns the fail-closed empty adapter before bootstrap has resolved", async () => {
159
+ // No __setMcpPublicWorkspaceForTest call — the module's initial
160
+ // { kind: "not-required" } state, exactly as an MCP tool invoked before
161
+ // apps/mcp/src/index.ts awaits initMcpPublicWorkspace() would see it (D4).
162
+ const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
163
+
164
+ expect(mockResolveContentAdapter).not.toHaveBeenCalled();
165
+ await expect(adapter.getPageBySlug("HomePage", "home")).resolves.toBeNull();
166
+ });
167
+ });
168
+
169
+ describe("getMcpMdxContentAdapter", () => {
170
+ beforeEach(() => {
171
+ mockResolveContentAdapter.mockReset();
172
+ });
173
+
174
+ it("returns the resolved adapter when it exposes getPageSource", () => {
175
+ const mdxish = { getPageSource: jest.fn() };
176
+ mockResolveContentAdapter.mockReturnValue(mdxish);
177
+
178
+ expect(getMcpMdxContentAdapter({ contentRoot: "/fixtures/content" })).toBe(
179
+ mdxish,
180
+ );
181
+ });
182
+
183
+ it("throws a named error when the resolved adapter is not MDX-capable", () => {
184
+ mockResolveContentAdapter.mockReturnValue({ getPageBySlug: jest.fn() });
185
+
186
+ expect(() => getMcpMdxContentAdapter()).toThrow(/CONTENT_ADAPTER=mdx/);
187
+ });
188
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * AC-2's "every content tool" sweep, demonstrated rather than argued (#5876
3
+ * spec D5, §4.1). `read_content.test.ts` and `list_pages.test.ts` (`##
4
+ * Scope`) already cover two of the nine read tools' database-path behaviour;
5
+ * this table drives all nine plus `buildSinglePageAuditInput` (the `audit`
6
+ * tool's page reader) through the SAME unresolved arm and asserts each
7
+ * returns its own not-found/empty shape — never a client-supplied workspace,
8
+ * never an enumeration signal, never a query.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+
13
+ import {
14
+ resetResolvedContentAdapter,
15
+ type ArtifactPrismaClient,
16
+ } from "@working-theory/content-adapters";
17
+
18
+ import { buildSinglePageAuditInput } from "../../src/lib/audit-single-page.js";
19
+ import {
20
+ __setMcpPublicWorkspaceForTest,
21
+ resetMcpPublicWorkspace,
22
+ } from "../../src/lib/public-workspace.js";
23
+ import { ailkGetBusinessHours } from "../../src/tools/get_business_hours.js";
24
+ import { ailkGetFaq } from "../../src/tools/get_faq.js";
25
+ import { ailkGetLocations } from "../../src/tools/get_locations.js";
26
+ import { ailkGetOfferings } from "../../src/tools/get_offerings.js";
27
+ import { ailkGetPricing } from "../../src/tools/get_pricing.js";
28
+ import { ailkGetSiteInfo } from "../../src/tools/get_site_info.js";
29
+ import { ailkListPages } from "../../src/tools/list_pages.js";
30
+ import { ailkReadContent } from "../../src/tools/read_content.js";
31
+ import { ailkSearchContent } from "../../src/tools/search_content.js";
32
+
33
+ /** A structural stub — never resolved into the seam under this arm, so every
34
+ * one of its methods must remain untouched by every case below. */
35
+ function makeArtifactPrismaClientStub(): ArtifactPrismaClient {
36
+ return {
37
+ artifact: {
38
+ findFirst: jest.fn(),
39
+ findMany: jest.fn(),
40
+ create: jest.fn(),
41
+ updateMany: jest.fn(),
42
+ deleteMany: jest.fn(),
43
+ },
44
+ artifactVersion: {
45
+ findFirst: jest.fn(),
46
+ findMany: jest.fn(),
47
+ create: jest.fn(),
48
+ updateMany: jest.fn(),
49
+ },
50
+ $transaction: jest.fn(),
51
+ };
52
+ }
53
+
54
+ describe("every content tool — CONTENT_ADAPTER=database, unresolved workspace", () => {
55
+ const previous = process.env.CONTENT_ADAPTER;
56
+ let stub: ArtifactPrismaClient;
57
+
58
+ beforeEach(() => {
59
+ process.env.CONTENT_ADAPTER = "database";
60
+ resetResolvedContentAdapter();
61
+ stub = makeArtifactPrismaClientStub();
62
+ __setMcpPublicWorkspaceForTest({ kind: "unresolved", reason: "ambiguous" });
63
+ });
64
+
65
+ afterEach(() => {
66
+ if (previous === undefined) delete process.env.CONTENT_ADAPTER;
67
+ else process.env.CONTENT_ADAPTER = previous;
68
+ resetMcpPublicWorkspace();
69
+ resetResolvedContentAdapter();
70
+ });
71
+
72
+ /** Every stub method is asserted untouched — nothing here reaches a query,
73
+ * not even the stub the deployment never got wired to (NFR-5). */
74
+ function expectNoQuery(): void {
75
+ expect(stub.artifact.findFirst).not.toHaveBeenCalled();
76
+ expect(stub.artifact.findMany).not.toHaveBeenCalled();
77
+ expect(stub.artifactVersion.findFirst).not.toHaveBeenCalled();
78
+ expect(stub.artifactVersion.findMany).not.toHaveBeenCalled();
79
+ }
80
+
81
+ it("ailkReadContent returns null", async () => {
82
+ const result = await ailkReadContent({ type: "HomePage", slug: "home" });
83
+ expect(result).toBeNull();
84
+ expectNoQuery();
85
+ });
86
+
87
+ it("ailkListPages returns { pages: [] }", async () => {
88
+ const result = await ailkListPages({ type: "HomePage" });
89
+ expect(result).toEqual({ pages: [] });
90
+ expectNoQuery();
91
+ });
92
+
93
+ it("ailkSearchContent returns { results: [] }", async () => {
94
+ const result = await ailkSearchContent({ query: "anything" });
95
+ expect(result).toEqual({ results: [] });
96
+ expectNoQuery();
97
+ });
98
+
99
+ it("ailkGetFaq returns null", async () => {
100
+ const result = await ailkGetFaq({});
101
+ expect(result).toBeNull();
102
+ expectNoQuery();
103
+ });
104
+
105
+ it("ailkGetOfferings returns { offerings: [] }", async () => {
106
+ const result = await ailkGetOfferings({});
107
+ expect(result).toEqual({ offerings: [] });
108
+ expectNoQuery();
109
+ });
110
+
111
+ it("ailkGetSiteInfo returns null", async () => {
112
+ const result = await ailkGetSiteInfo({});
113
+ expect(result).toBeNull();
114
+ expectNoQuery();
115
+ });
116
+
117
+ it("ailkGetBusinessHours returns null", async () => {
118
+ const result = await ailkGetBusinessHours({});
119
+ expect(result).toBeNull();
120
+ expectNoQuery();
121
+ });
122
+
123
+ it("ailkGetLocations returns { locations: [] }", async () => {
124
+ const result = await ailkGetLocations({});
125
+ expect(result).toEqual({ locations: [] });
126
+ expectNoQuery();
127
+ });
128
+
129
+ it("ailkGetPricing returns null", async () => {
130
+ const result = await ailkGetPricing({});
131
+ expect(result).toBeNull();
132
+ expectNoQuery();
133
+ });
134
+
135
+ it("buildSinglePageAuditInput throws its own not-found error (never a workspace-state error)", async () => {
136
+ await expect(buildSinglePageAuditInput({ slug: "home" })).rejects.toThrow(
137
+ /Page not found/,
138
+ );
139
+ expectNoQuery();
140
+ });
141
+
142
+ it("no content tool source names workspaceId (structural guard — D8)", () => {
143
+ // The tools imported above — a caller-supplied workspaceId is never a
144
+ // valid MCP input, on any tool, at any point (mirrors read_content.test.ts's
145
+ // existing "no console.*" structural guard idiom).
146
+ const toolFiles = [
147
+ "read_content.ts",
148
+ "list_pages.ts",
149
+ "search_content.ts",
150
+ "get_faq.ts",
151
+ "get_offerings.ts",
152
+ "get_site_info.ts",
153
+ "get_business_hours.ts",
154
+ "get_locations.ts",
155
+ "get_pricing.ts",
156
+ ];
157
+ for (const file of toolFiles) {
158
+ const src = readFileSync(
159
+ resolve(__dirname, "../../src/tools", file),
160
+ "utf-8",
161
+ );
162
+ expect(src).not.toMatch(/\bworkspaceId\b/);
163
+ }
164
+ });
165
+ });