create-ailk 0.22.0 → 0.24.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 (287) hide show
  1. package/README.md +27 -14
  2. package/component-catalog.json +149 -5
  3. package/component-catalog.md +7 -6
  4. package/dist/add-module.js +17 -10
  5. package/dist/add-route.d.ts +55 -1
  6. package/dist/add-route.js +301 -13
  7. package/dist/cli.js +27 -10
  8. package/dist/derive-architecture.d.ts +7 -5
  9. package/dist/derive-architecture.js +26 -32
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +0 -4
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +36 -2
  15. package/dist/lib/scaffold-render-sweep.js +50 -8
  16. package/dist/module-architecture.js +115 -10
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/seed-flows.d.ts +4 -1
  20. package/dist/seed-flows.js +50 -4
  21. package/dist/spoke-checkout.d.ts +38 -35
  22. package/dist/spoke-checkout.js +66 -229
  23. package/dist/starter-grid.d.ts +0 -3
  24. package/dist/starter-grid.js +24 -14
  25. package/dist/surfaces.js +33 -12
  26. package/dist/sync-routes.d.ts +68 -5
  27. package/dist/sync-routes.js +455 -29
  28. package/dist/template-record.d.ts +1 -1
  29. package/dist/template-record.js +3 -2
  30. package/dist/update.js +10 -0
  31. package/package.json +3 -3
  32. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  33. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  35. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  38. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  40. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  41. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  43. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  44. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  46. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  47. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  48. package/templates/.env.example +13 -7
  49. package/templates/.github/workflows/ci.yml +13 -0
  50. package/templates/CLAUDE.md +4 -2
  51. package/templates/README.md +1 -0
  52. package/templates/_gitignore +4 -0
  53. package/templates/apps/api/.env.example +34 -0
  54. package/templates/apps/api/CLAUDE.md +94 -19
  55. package/templates/apps/api/package.json +1 -0
  56. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  57. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  58. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  59. package/templates/apps/api/src/config/index.ts +109 -0
  60. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
  61. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  62. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  63. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  64. package/templates/apps/api/src/lib/site.ts +4 -1
  65. package/templates/apps/api/src/lib/stripe.ts +77 -4
  66. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  67. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  68. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  69. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  70. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  71. package/templates/apps/api/src/openapi/spec.ts +343 -7
  72. package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
  73. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  74. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
  75. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  76. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  77. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  78. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  79. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  80. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  81. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  82. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  83. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  84. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  85. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  87. package/templates/apps/api/src/routes/content/index.ts +16 -2
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  89. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  90. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  91. package/templates/apps/api/src/routes/content/search.ts +16 -45
  92. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  93. package/templates/apps/api/src/routes/leads/index.ts +377 -308
  94. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  95. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  96. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  97. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  98. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  99. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  100. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  101. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
  102. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  103. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  104. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  105. package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
  106. package/templates/apps/api/src/server.ts +165 -51
  107. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  108. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  109. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
  110. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  111. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  112. package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
  113. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
  114. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  115. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  116. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  117. package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
  118. package/templates/apps/api/src/services/entitlements.ts +157 -1
  119. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  120. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
  121. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  122. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
  123. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  124. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
  125. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
  126. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  127. package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
  128. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
  129. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  130. package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
  131. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  132. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  133. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  134. package/templates/apps/api/src/services/person.ts +147 -9
  135. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  136. package/templates/apps/api/src/services/project-listings.ts +43 -31
  137. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  138. package/templates/apps/mcp/CLAUDE.md +32 -18
  139. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  140. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  141. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  142. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  143. package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
  144. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  145. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  146. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  147. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  148. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  149. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  150. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  151. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  152. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  153. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  154. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  155. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  156. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  157. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  158. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  159. package/templates/apps/mcp/package.json +1 -0
  160. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  161. package/templates/apps/mcp/src/http-transport.ts +47 -9
  162. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  163. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  164. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  165. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  166. package/templates/apps/mcp/src/server.ts +5 -0
  167. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  168. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  169. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  170. package/templates/apps/mcp/src/tools/index.ts +22 -21
  171. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  172. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  173. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  174. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  175. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  176. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  177. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  178. package/templates/apps/web/.env.example +10 -31
  179. package/templates/apps/web/CLAUDE.md +324 -42
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  183. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  184. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  185. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  186. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  187. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  188. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  189. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  190. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  191. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  192. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  193. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  194. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  195. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  196. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  197. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
  198. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  199. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  200. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  201. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  202. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  203. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  204. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  205. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  206. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
  207. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  208. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  209. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  210. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  211. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  212. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  213. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  214. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  215. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  216. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  217. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  218. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  219. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  220. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  221. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  222. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  223. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  224. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  225. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  226. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  227. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
  228. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  229. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  230. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  231. package/templates/apps/web/lib/auth-routes.ts +36 -2
  232. package/templates/apps/web/lib/checkout-config.ts +9 -20
  233. package/templates/apps/web/lib/deck-page.ts +1 -0
  234. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  235. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  236. package/templates/apps/web/lib/magnet-routes.ts +44 -0
  237. package/templates/apps/web/lib/modules.generated.ts +9 -0
  238. package/templates/apps/web/lib/newsletter.actions.ts +205 -0
  239. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  240. package/templates/apps/web/lib/posting-title.ts +17 -0
  241. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  242. package/templates/apps/web/messages/en.json +49 -15
  243. package/templates/apps/web/middleware.ts +12 -2
  244. package/templates/apps/web/security-headers.config.ts +17 -0
  245. package/templates/content/en/BlogList/blog.mdx +4 -71
  246. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  247. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  248. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  249. package/templates/content/en/BookCall/start-project.mdx +1 -17
  250. package/templates/content/en/ContactPage/contact.mdx +6 -63
  251. package/templates/content/en/HomePage/home.mdx +3 -21
  252. package/templates/database/CHANGELOG.md +51 -0
  253. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  254. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  255. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  256. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  257. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  258. package/templates/database/__tests__/seed.test.ts +63 -1
  259. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  260. package/templates/database/auth/schema.prisma +4 -0
  261. package/templates/database/config/schema.prisma +91 -0
  262. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  263. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  264. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  265. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  266. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  267. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  268. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  269. package/templates/database/package.json +1 -1
  270. package/templates/database/seed.ts +57 -2
  271. package/templates/tools/lint-architecture.ts +205 -1
  272. package/versions.json +35 -35
  273. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  274. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  275. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  276. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  277. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  278. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  279. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  280. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  281. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  282. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  283. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  284. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  285. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  286. package/templates/apps/web/lib/checkout.ts +0 -93
  287. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -13,7 +13,17 @@
13
13
  * - the global and per-app `forbidden_patterns` (with `paths_excluded`);
14
14
  * - the `namespaces` boundaries inside `database/`, when declared:
15
15
  * cross-namespace relative imports and cross-namespace Prisma `@relation`
16
- * directives.
16
+ * directives;
17
+ * - the `upgradeable_consumer` rules, when declared — this site builds on
18
+ * the published `@working-theory/*` packages and never forks them:
19
+ * every `pnpm-workspace.yaml` glob is inside the declared `workspaces`;
20
+ * no `package.json` outside `node_modules` takes an in-scope name the
21
+ * scaffold did not create (`scaffold_packages`); no `patchedDependencies`
22
+ * entry (root `package.json` `pnpm` block or `pnpm-workspace.yaml`)
23
+ * patches an in-scope package; and every declaration of one in-scope
24
+ * package (dependencies, devDependencies, peerDependencies,
25
+ * optionalDependencies, `pnpm.overrides`, `resolutions`, workspace
26
+ * `overrides`) uses the same caret range.
17
27
  *
18
28
  * An import target is checked only when it is a workspace present in this
19
29
  * repo (`apps/*`, `database`); `@working-theory/*` libraries install from npm
@@ -45,10 +55,17 @@ interface AppRule {
45
55
  forbidden_patterns?: ForbiddenPattern[];
46
56
  }
47
57
 
58
+ interface UpgradeableConsumer {
59
+ package_scope?: string;
60
+ workspaces?: string[];
61
+ scaffold_packages?: string[];
62
+ }
63
+
48
64
  interface ArchConfig {
49
65
  apps?: Record<string, AppRule>;
50
66
  forbidden_patterns?: ForbiddenPattern[];
51
67
  namespaces?: { members?: Record<string, { path: string }> };
68
+ upgradeable_consumer?: UpgradeableConsumer;
52
69
  }
53
70
 
54
71
  interface Violation {
@@ -60,6 +77,9 @@ interface Violation {
60
77
 
61
78
  const ROOT = resolve(dirname(process.argv[1] ?? "."), "..");
62
79
  const SKIP_DIRS = new Set([
80
+ ".git",
81
+ ".pnpm-store",
82
+ ".vercel",
63
83
  "node_modules",
64
84
  "dist",
65
85
  ".next",
@@ -266,6 +286,189 @@ function checkNamespaces(cfg: ArchConfig): Violation[] {
266
286
  return out;
267
287
  }
268
288
 
289
+ // ── Upgradeable consumer (#9191) ─────────────────────────────────────────
290
+ //
291
+ // A copied, patched or pinned-off package stops taking upstream fixes, and
292
+ // whoever made the copy believes it is harmless. These rules refuse each form.
293
+
294
+ interface PackageJson {
295
+ name?: unknown;
296
+ pnpm?: { patchedDependencies?: unknown; overrides?: unknown };
297
+ resolutions?: unknown;
298
+ [field: string]: unknown;
299
+ }
300
+
301
+ interface Declaration {
302
+ file: string;
303
+ line: number;
304
+ name: string;
305
+ range: string;
306
+ field: string;
307
+ }
308
+
309
+ const DEP_FIELDS = [
310
+ "dependencies",
311
+ "devDependencies",
312
+ "peerDependencies",
313
+ "optionalDependencies",
314
+ ] as const;
315
+ const CARET_RANGE = /^\^\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?$/;
316
+
317
+ /** The 1-based line of the first line containing `needle`, else 1. */
318
+ function lineOfText(text: string, needle: string): number {
319
+ const i = text.split("\n").findIndex((line) => line.includes(needle));
320
+ return i === -1 ? 1 : i + 1;
321
+ }
322
+
323
+ /**
324
+ * The 1-based line of `key` as a map key at or after the first line holding
325
+ * `section` (a field label), so a package declared in two fields of one file
326
+ * reports each at its own line. Falls back to the first mention of `key`.
327
+ */
328
+ function lineOfKey(text: string, section: string, key: string): number {
329
+ const lines = text.split("\n");
330
+ const start = Math.max(0, lines.findIndex((line) => line.includes(section)));
331
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
332
+ const keyRe = new RegExp(`(?:^|[\\s{,])["']?${escaped}["']?\\s*:`);
333
+ const i = lines.findIndex((line, n) => n >= start && keyRe.test(line));
334
+ return i === -1 ? lineOfText(text, key) : i + 1;
335
+ }
336
+
337
+ /** A map-valued field as string entries, or none. */
338
+ function stringEntries(value: unknown): [string, string][] {
339
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
340
+ return Object.entries(value).filter(
341
+ (e): e is [string, string] => typeof e[1] === "string",
342
+ );
343
+ }
344
+
345
+ /** A patch or override key (`@s/p@1.2.3`, `a>@s/p@^1`) → the package it targets. */
346
+ function packageOfKey(key: string): string {
347
+ const target = key.slice(key.lastIndexOf(">") + 1);
348
+ const at = target.lastIndexOf("@");
349
+ return at > 0 ? target.slice(0, at) : target;
350
+ }
351
+
352
+ /** True when `glob` reaches nothing outside one of the `allowed` workspace globs. */
353
+ function insideWorkspaces(glob: string, allowed: string[]): boolean {
354
+ const g = glob.replace(/^\.\//, "").replace(/\/$/, "");
355
+ return allowed.some((a) => {
356
+ if (g === a) return true;
357
+ if (!a.endsWith("/*")) return false;
358
+ const prefix = a.slice(0, -1);
359
+ const rest = g.slice(prefix.length);
360
+ return g.startsWith(prefix) && rest !== "" && !rest.includes("/") && !rest.includes("**");
361
+ });
362
+ }
363
+
364
+ function checkUpgradeableConsumer(cfg: ArchConfig): Violation[] {
365
+ const uc = cfg.upgradeable_consumer;
366
+ if (!uc) return [];
367
+ const scope = uc.package_scope ?? "@working-theory/";
368
+ const workspaces = uc.workspaces ?? [];
369
+ const created = new Set(uc.scaffold_packages ?? []);
370
+ const inScope = (name: string) => name.startsWith(scope);
371
+ const out: Violation[] = [];
372
+ const decls: Declaration[] = [];
373
+
374
+ const patches = (file: string, text: string, value: unknown) => {
375
+ for (const [key] of stringEntries(value)) {
376
+ if (!inScope(packageOfKey(key))) continue;
377
+ out.push({
378
+ file,
379
+ line: lineOfKey(text, "patchedDependencies", key),
380
+ rule: `patchedDependencies patches "${key}"`,
381
+ reason:
382
+ "Fix: remove the patch — a patched package stops taking upstream fixes. Wrap or configure it from your app, or report the defect upstream.",
383
+ });
384
+ }
385
+ };
386
+ const declare = (file: string, text: string, field: string, value: unknown, byKey: boolean) => {
387
+ for (const [key, range] of stringEntries(value)) {
388
+ const name = byKey ? packageOfKey(key) : key;
389
+ if (!inScope(name)) continue;
390
+ // A scaffold's own workspace may be linked locally; anything else may not.
391
+ if (created.has(name) && range.startsWith("workspace:")) continue;
392
+ const line = lineOfKey(text, field.slice(field.lastIndexOf(".") + 1), key);
393
+ decls.push({ file, line, name, range, field });
394
+ }
395
+ };
396
+
397
+ const wsPath = join(ROOT, "pnpm-workspace.yaml");
398
+ if (existsSync(wsPath)) {
399
+ const text = readFileSync(wsPath, "utf8");
400
+ const ws = (parseYaml(text) ?? {}) as {
401
+ packages?: unknown;
402
+ patchedDependencies?: unknown;
403
+ overrides?: unknown;
404
+ };
405
+ for (const glob of Array.isArray(ws.packages) ? ws.packages : []) {
406
+ if (typeof glob !== "string" || glob.startsWith("!")) continue;
407
+ if (insideWorkspaces(glob, workspaces)) continue;
408
+ out.push({
409
+ file: wsPath,
410
+ line: lineOfText(text, glob),
411
+ rule: `workspace glob "${glob}" is outside this site's workspaces (${workspaces.join(", ")})`,
412
+ reason: `Fix: remove it. A site consumes ${scope}* from npm — compose, wrap or configure the published package from an app instead of keeping a local package tree.`,
413
+ });
414
+ }
415
+ patches(wsPath, text, ws.patchedDependencies);
416
+ declare(wsPath, text, "overrides", ws.overrides, true);
417
+ }
418
+
419
+ for (const file of walk(ROOT, /^package\.json$/)) {
420
+ const text = readFileSync(file, "utf8");
421
+ let pkg: PackageJson;
422
+ try {
423
+ pkg = JSON.parse(text) as PackageJson;
424
+ } catch {
425
+ continue; // pnpm itself refuses an unparseable manifest
426
+ }
427
+ if (typeof pkg.name === "string" && inScope(pkg.name) && !created.has(pkg.name)) {
428
+ out.push({
429
+ file,
430
+ line: lineOfText(text, `"${pkg.name}"`),
431
+ rule: `local package named "${pkg.name}" — not a package this scaffold created (${[...created].join(", ")})`,
432
+ reason: `Fix: delete the local copy and depend on the published ${pkg.name}; build on it by composing, wrapping or configuring it through its documented extension points — never copy it out of node_modules.`,
433
+ });
434
+ }
435
+ for (const field of DEP_FIELDS) declare(file, text, field, pkg[field], false);
436
+ declare(file, text, "pnpm.overrides", pkg.pnpm?.overrides, true);
437
+ declare(file, text, "resolutions", pkg.resolutions, true);
438
+ patches(file, text, pkg.pnpm?.patchedDependencies);
439
+ }
440
+
441
+ const byName = new Map<string, Declaration[]>();
442
+ for (const d of decls) {
443
+ if (!CARET_RANGE.test(d.range)) {
444
+ const example = /^\d/.test(d.range) ? `^${d.range}` : "^<version>";
445
+ out.push({
446
+ file: d.file,
447
+ line: d.line,
448
+ rule: `${d.name} is "${d.range}" in ${d.field}, not a caret range`,
449
+ reason: `Fix: declare it as a caret range (e.g. "${example}") so upgrades flow; never pin it, link it or point it at a local copy.`,
450
+ });
451
+ continue;
452
+ }
453
+ const list = byName.get(d.name) ?? [];
454
+ list.push(d);
455
+ byName.set(d.name, list);
456
+ }
457
+ for (const [name, list] of byName) {
458
+ if (new Set(list.map((d) => d.range)).size < 2) continue;
459
+ const where = list.map((d) => `${rel(d.file)} ${d.field} "${d.range}"`).join(", ");
460
+ for (const d of list) {
461
+ out.push({
462
+ file: d.file,
463
+ line: d.line,
464
+ rule: `${name} is declared on different ranges: ${where}`,
465
+ reason: `Fix: align every declaration of ${name} on one caret range.`,
466
+ });
467
+ }
468
+ }
469
+ return out;
470
+ }
471
+
269
472
  function main(): void {
270
473
  const yamlPath = join(ROOT, "architecture.yaml");
271
474
  if (!existsSync(yamlPath)) {
@@ -297,6 +500,7 @@ function main(): void {
297
500
  }
298
501
  }
299
502
  violations.push(...checkNamespaces(cfg));
503
+ violations.push(...checkUpgradeableConsumer(cfg));
300
504
 
301
505
  if (violations.length === 0) {
302
506
  console.log("architecture: OK");
package/versions.json CHANGED
@@ -1,53 +1,53 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.43.0",
3
- "@working-theory/aeo": "0.43.0",
4
- "@working-theory/agent-context": "0.43.0",
2
+ "@working-theory/admin-ui": "0.45.0",
3
+ "@working-theory/aeo": "0.45.0",
4
+ "@working-theory/agent-context": "0.45.0",
5
5
  "@working-theory/agent-contract": "0.1.0",
6
6
  "@working-theory/agent-memory": "0.0.0",
7
7
  "@working-theory/ai": "0.1.0",
8
- "@working-theory/analytics": "0.43.0",
9
- "@working-theory/api-client": "0.43.0",
8
+ "@working-theory/analytics": "0.45.0",
9
+ "@working-theory/api-client": "0.45.0",
10
10
  "@working-theory/cda-filesystem": "0.0.0",
11
- "@working-theory/cli": "0.43.0",
12
- "@working-theory/common": "0.43.0",
11
+ "@working-theory/cli": "0.45.0",
12
+ "@working-theory/common": "0.45.0",
13
13
  "@working-theory/consent": "0.1.0",
14
- "@working-theory/content-adapters": "0.43.0",
15
- "@working-theory/content-projection": "0.43.0",
16
- "@working-theory/email": "0.43.0",
14
+ "@working-theory/content-adapters": "0.45.0",
15
+ "@working-theory/content-projection": "0.45.0",
16
+ "@working-theory/email": "0.45.0",
17
17
  "@working-theory/env": "0.0.0",
18
- "@working-theory/eslint-config": "0.43.0",
19
- "@working-theory/fumadocs-schema": "0.43.0",
20
- "@working-theory/ids": "0.14.0",
21
- "@working-theory/jsonld": "0.43.0",
22
- "@working-theory/license": "0.43.0",
23
- "@working-theory/metadata": "0.43.0",
24
- "@working-theory/observability": "0.43.0",
25
- "@working-theory/page-renderer": "0.43.0",
18
+ "@working-theory/eslint-config": "0.45.0",
19
+ "@working-theory/fumadocs-schema": "0.45.0",
20
+ "@working-theory/ids": "0.15.0",
21
+ "@working-theory/jsonld": "0.45.0",
22
+ "@working-theory/license": "0.45.0",
23
+ "@working-theory/metadata": "0.45.0",
24
+ "@working-theory/observability": "0.45.0",
25
+ "@working-theory/page-renderer": "0.45.0",
26
26
  "@working-theory/plugin-mcp": "0.1.3",
27
- "@working-theory/schema": "0.43.0",
28
- "@working-theory/schema-pack-registry": "0.43.0",
29
- "@working-theory/schema-validator": "0.43.0",
30
- "@working-theory/sections-registry": "0.43.0",
31
- "@working-theory/site-config": "0.43.0",
32
- "@working-theory/spoke-checkout": "0.6.0",
33
- "@working-theory/stripe-client": "0.43.0",
34
- "@working-theory/templates": "0.43.0",
35
- "@working-theory/theme": "0.43.0",
36
- "@working-theory/ui": "0.43.0",
37
- "@working-theory/validation": "0.43.0",
38
- "@working-theory/web-i18n": "0.43.0",
27
+ "@working-theory/schema": "0.45.0",
28
+ "@working-theory/schema-pack-registry": "0.45.0",
29
+ "@working-theory/schema-validator": "0.45.0",
30
+ "@working-theory/sections-registry": "0.45.0",
31
+ "@working-theory/site-config": "0.45.0",
32
+ "@working-theory/spoke-checkout": "0.7.1",
33
+ "@working-theory/stripe-client": "0.45.0",
34
+ "@working-theory/templates": "0.45.0",
35
+ "@working-theory/theme": "0.45.0",
36
+ "@working-theory/ui": "0.45.0",
37
+ "@working-theory/validation": "0.45.0",
38
+ "@working-theory/web-i18n": "0.45.0",
39
39
  "@working-theory/billing-client": "0.0.0",
40
40
  "@working-theory/branding": "0.0.0",
41
41
  "@working-theory/license-client": "0.0.0",
42
42
  "@working-theory/license-manifest": "0.0.0",
43
43
  "@working-theory/partner-session": "0.0.0",
44
44
  "@working-theory/preview-protocol": "0.0.0",
45
- "@working-theory/ui-commerce": "0.23.21",
46
- "@working-theory/ui-docs-blog": "0.23.21",
47
- "@working-theory/ui-scheduling": "0.24.1",
48
- "@working-theory/ui-signal-collectors": "0.26.0",
45
+ "@working-theory/ui-commerce": "0.23.23",
46
+ "@working-theory/ui-docs-blog": "0.23.23",
47
+ "@working-theory/ui-scheduling": "0.24.3",
48
+ "@working-theory/ui-signal-collectors": "0.27.1",
49
49
  "@working-theory/web": "0.0.0",
50
50
  "@working-theory/api": "1.0.0",
51
51
  "@working-theory/mcp": "0.0.0",
52
- "@working-theory/database": "0.43.0"
52
+ "@working-theory/database": "0.45.0"
53
53
  }
@@ -1,199 +0,0 @@
1
- ---
2
- type: "Pricing"
3
- slug: "pricing"
4
- title: "{Upgrade to Pro with Commerce}"
5
- description: "{Checkout isn't included on the Free template. Upgrade to Pro with Commerce and take payments on your site with Stripe.}"
6
- sections:
7
- [
8
- {
9
- "id": "hero",
10
- "type": "hero",
11
- "data":
12
- {
13
- "variant": "centered",
14
- "gapBelow": "tight",
15
- "eyebrow": "Pricing",
16
- "headline": "{Upgrade to Pro with Commerce}",
17
- "subheadline": "{Checkout isn't included on the Free template. Upgrade to Pro with Commerce and take payments on your site with Stripe.}",
18
- "primaryCta":
19
- {
20
- "label": "{Add Commerce for SMB}",
21
- "href": "https://ailaunchkit.ai/pricing",
22
- },
23
- "secondaryCta":
24
- {
25
- "label": "{Add Commerce for SaaS}",
26
- "href": "https://ailaunchkit.ai/pricing",
27
- "variant": "outline",
28
- },
29
- },
30
- },
31
- {
32
- "id": "plans",
33
- "type": "pricing",
34
- "data":
35
- {
36
- "variant": "three-tiers-with-toggle",
37
- "billingToggle": true,
38
- "toggleStyle": "switch",
39
- "gapAbove": "tight",
40
- "cardLayout": "centered",
41
- "plans":
42
- [
43
- {
44
- "name": "Basic plan",
45
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
46
- "features":
47
- [
48
- "{A feature this plan includes}",
49
- "{A feature this plan includes}",
50
- "{A feature this plan includes}",
51
- ],
52
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
53
- },
54
- {
55
- "name": "Business plan",
56
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
57
- "features":
58
- [
59
- "{A feature this plan adds}",
60
- "{A feature this plan includes}",
61
- "{A feature this plan includes}",
62
- "{A feature this plan includes}",
63
- ],
64
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
65
- },
66
- {
67
- "name": "Enterprise plan",
68
- "price": { "monthly": "{price}/mo", "annual": "{price}/yr" },
69
- "features":
70
- [
71
- "{A feature this plan adds}",
72
- "{A feature this plan includes}",
73
- "{A feature this plan includes}",
74
- "{A feature this plan includes}",
75
- "{A feature this plan includes}",
76
- ],
77
- "cta": { "label": "{Primary CTA}", "href": "/contact" },
78
- },
79
- ],
80
- },
81
- },
82
- {
83
- "id": "logo-cloud",
84
- "type": "logoCloud",
85
- "data":
86
- {
87
- "headline": "{Don’t just take our word for it}",
88
- "headlineStyle": "label",
89
- "logos":
90
- [
91
- {
92
- "name": "SalesSmyth",
93
- "src": "/logos/salessmyth.svg",
94
- "darkSrc": "/logos/salessmyth-light.svg",
95
- "width": 1118,
96
- "height": 147,
97
- },
98
- {
99
- "name": "AI Launch Kit",
100
- "src": "/logos/ai-launch-kit.svg",
101
- "darkSrc": "/logos/ai-launch-kit-light.svg",
102
- "width": 1248,
103
- "height": 144,
104
- },
105
- {
106
- "name": "Atomic Agent",
107
- "src": "/logos/atomic-agent.svg",
108
- "darkSrc": "/logos/atomic-agent-light.svg",
109
- "width": 1320,
110
- "height": 148,
111
- },
112
- ],
113
- "layout": "inline",
114
- "treatment": "monochrome",
115
- },
116
- },
117
- {
118
- "id": "testimonial",
119
- "type": "testimonials",
120
- "data":
121
- {
122
- "variant": "quote",
123
- "items":
124
- [
125
- {
126
- "quote": "{A customer quote, in their own words, about the result they got}",
127
- "author": "{Customer name}",
128
- "role": "{Their title, their company}",
129
- },
130
- ],
131
- },
132
- },
133
- {
134
- "id": "switch",
135
- "type": "cta",
136
- "data":
137
- {
138
- "variant": "box",
139
- "headline": "{Switch to your product in time amount or less}",
140
- "description": "{You shouldn't have to experience a painful problem. With your product, you can finally achieve a value outcome or get a big improvement.}",
141
- "checklist":
142
- ["No credit card required", "Cancel any time", "Live chat support"],
143
- "primaryCta": { "label": "{Primary CTA}", "href": "/contact" },
144
- "secondaryCta":
145
- {
146
- "label": "{Secondary CTA}",
147
- "href": "/pricing",
148
- "variant": "outline",
149
- },
150
- "closingLine": "{Social Proof goes here}",
151
- },
152
- },
153
- {
154
- "id": "faq",
155
- "type": "faq",
156
- "data":
157
- {
158
- "headline": "{FAQs}",
159
- "description": "{One sentence that invites visitors to ask what this list does not answer}",
160
- "actions":
161
- [
162
- {
163
- "kind": "button",
164
- "label": "Contact",
165
- "href": "/contact",
166
- "variant": "outline",
167
- },
168
- ],
169
- "presentation": "accordion",
170
- "initialOpen": "none",
171
- "items":
172
- [
173
- {
174
- "question": "{A question your customers actually ask}",
175
- "answer": "{Answer the question in two or three plain sentences}",
176
- },
177
- {
178
- "question": "{A question your customers actually ask}",
179
- "answer": "{Answer the question in two or three plain sentences}",
180
- },
181
- {
182
- "question": "{A question your customers actually ask}",
183
- "answer": "{Answer the question in two or three plain sentences}",
184
- },
185
- {
186
- "question": "{A question your customers actually ask}",
187
- "answer": "{Answer the question in two or three plain sentences}",
188
- },
189
- {
190
- "question": "{A question your customers actually ask}",
191
- "answer": "{Answer the question in two or three plain sentences}",
192
- },
193
- ],
194
- },
195
- },
196
- ]
197
- ---
198
-
199
- {/* Rendered from frontmatter `sections[]`. Seed content — replace the plan names, prices and features above with your own before you launch. */}
@@ -1,9 +0,0 @@
1
- /**
2
- * Slug allowlist for D14a content-write routes.
3
- *
4
- * Re-exported from `@working-theory/content-adapters` (single source of truth, per
5
- * issue #398). The adapter sanitizes slug + type before any path
6
- * construction or git argument; this regex is the route-layer fail-fast
7
- * mirror so malformed input gets a 400 before reaching the adapter.
8
- */
9
- export { SLUG_PATTERN } from "@working-theory/content-adapters";
@@ -1,93 +0,0 @@
1
- /**
2
- * HTML snapshot regression tests for the downloads route →
3
- * @working-theory/page-renderer renderPage({ bodyMode: "mdx-body" }) migration.
4
- *
5
- * Two-commit strategy:
6
- * Commit 1: snapshots captured against pre-migration route (baseline).
7
- * Commit 2: route migrated; same snapshots verify diff=0.
8
- *
9
- * Routes covered:
10
- * - downloads/page.tsx (empty + populated — 2 snapshots)
11
- *
12
- * Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
13
- */
14
- import React from "react";
15
- import { render } from "@testing-library/react";
16
-
17
- // ── next-mdx-remote/rsc mock ──────────────────────────────────────────────────
18
- jest.mock("next-mdx-remote/rsc", () => ({
19
- MDXRemote: ({ source }: { source: string }): React.ReactElement =>
20
- React.createElement("div", { "data-testid": "mdx-rendered" }, source),
21
- }));
22
-
23
- // ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
24
- jest.mock("@working-theory/ui", () => ({
25
- // #7745: renderPage's mdx-body branch frames its head and body in the
26
- // section Container and renders the body through MDXContent + the shared map.
27
- Container: ({
28
- children,
29
- }: {
30
- children?: React.ReactNode;
31
- }): React.ReactElement => React.createElement("div", null, children),
32
- MDXContent: ({
33
- children,
34
- }: {
35
- children?: React.ReactNode;
36
- }): React.ReactElement => React.createElement(React.Fragment, null, children),
37
- getMDXComponents: (overrides?: Record<string, unknown>) => ({ ...overrides }),
38
- // #4446/#4447: renderPage's own fallback/empty-state title now composes
39
- // Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
40
- Heading: ({
41
- children,
42
- level,
43
- }: {
44
- children: React.ReactNode;
45
- level: number;
46
- }): React.ReactElement => React.createElement(`h${level}`, null, children),
47
- SectionRenderer: ({ sections }: { sections: unknown[] }) =>
48
- React.createElement("div", {
49
- "data-testid": "section-renderer",
50
- "data-section-count": String(sections.length),
51
- }),
52
- }));
53
-
54
- import DownloadsPage from "../page";
55
-
56
- const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
57
-
58
- // ── DownloadsPage ─────────────────────────────────────────────────────────────
59
-
60
- describe("DownloadsPage — snapshot regression", () => {
61
- it("empty state renders deterministic HTML", async () => {
62
- const element = await DownloadsPage({
63
- params: Promise.resolve({ locale: "en" }),
64
- getMdxSource: getMdxSourceEmpty,
65
- });
66
- const { container } = render(element);
67
- expect(container.innerHTML).toMatchInlineSnapshot(
68
- `"<main id="main"><h1>Downloads</h1><p data-testid="section-empty-state">Downloads listing coming soon.</p></main>"`,
69
- );
70
- });
71
-
72
- it("populated state renders deterministic HTML", async () => {
73
- const getMdxSourcePopulated = jest.fn().mockResolvedValue({
74
- frontmatter: {
75
- type: "DownloadsList",
76
- slug: "downloads",
77
- title: "Downloads",
78
- description: "Downloadable assets and templates.",
79
- },
80
- body: "# Downloads\n\nBody.",
81
- });
82
-
83
- const element = await DownloadsPage({
84
- params: Promise.resolve({ locale: "en" }),
85
- getMdxSource: getMdxSourcePopulated,
86
- });
87
- const { container } = render(element);
88
- expect(container.innerHTML).toMatchInlineSnapshot(`
89
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Downloads","description":"Downloadable assets and templates.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"DigitalDocument"}]}}</script><main id="main"><div><h1>Downloads</h1><p data-testid="downloads-description" class="mt-block-tight text-lead">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">
90
- Body.</div></article></div></div></div></main>"
91
- `);
92
- });
93
- });