create-ailk 0.7.0 → 0.9.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 (310) hide show
  1. package/README.md +112 -10
  2. package/component-catalog.json +798 -49
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module.d.ts +20 -0
  5. package/dist/add-module.js +43 -7
  6. package/dist/add-route.d.ts +56 -0
  7. package/dist/add-route.js +286 -0
  8. package/dist/apply-design-md.d.ts +7 -0
  9. package/dist/apply-design-md.js +50 -0
  10. package/dist/apply-design.d.ts +64 -0
  11. package/dist/apply-design.js +273 -0
  12. package/dist/claude-md.d.ts +111 -0
  13. package/dist/claude-md.js +334 -0
  14. package/dist/cli.js +114 -28
  15. package/dist/derive-architecture.js +41 -36
  16. package/dist/design-compile.d.ts +71 -0
  17. package/dist/design-compile.js +98 -0
  18. package/dist/design-package.d.ts +83 -0
  19. package/dist/design-package.js +468 -0
  20. package/dist/design-presets.d.ts +86 -0
  21. package/dist/design-presets.js +642 -0
  22. package/dist/design-scaffold.d.ts +76 -0
  23. package/dist/design-scaffold.js +220 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +4 -1
  26. package/dist/legacy-nav-renderers.d.ts +45 -0
  27. package/dist/legacy-nav-renderers.js +545 -0
  28. package/dist/lib/auth-secret-file.d.ts +19 -0
  29. package/dist/lib/auth-secret-file.js +112 -0
  30. package/dist/lib/design-checks.d.ts +78 -0
  31. package/dist/lib/design-checks.js +174 -0
  32. package/dist/lib/extract-module-bundle.js +5 -1
  33. package/dist/lib/license-key-files.d.ts +9 -0
  34. package/dist/lib/license-key-files.js +2 -2
  35. package/dist/lib/module-license-gate.d.ts +89 -0
  36. package/dist/lib/module-license-gate.js +171 -12
  37. package/dist/lib/module-tier.d.ts +45 -16
  38. package/dist/lib/module-tier.js +76 -0
  39. package/dist/lib/pro-callout.js +10 -2
  40. package/dist/lib/render-verdict.d.ts +52 -0
  41. package/dist/lib/render-verdict.js +77 -0
  42. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  43. package/dist/lib/scaffold-render-sweep.js +236 -0
  44. package/dist/module-architecture.d.ts +19 -16
  45. package/dist/module-architecture.js +64 -21
  46. package/dist/parse-args.d.ts +88 -10
  47. package/dist/parse-args.js +272 -21
  48. package/dist/programmatic.d.ts +66 -12
  49. package/dist/programmatic.js +361 -34
  50. package/dist/site-chrome.d.ts +19 -9
  51. package/dist/site-chrome.js +23 -13
  52. package/dist/site-manifest.d.ts +25 -0
  53. package/dist/site-manifest.js +254 -0
  54. package/dist/site-roster.d.ts +41 -0
  55. package/dist/site-roster.js +149 -11
  56. package/dist/starter-grid.d.ts +232 -0
  57. package/dist/starter-grid.js +545 -0
  58. package/dist/surfaces.d.ts +41 -9
  59. package/dist/surfaces.js +268 -92
  60. package/dist/template-manifest.d.ts +65 -29
  61. package/dist/template-manifest.js +161 -382
  62. package/dist/template-names.d.ts +14 -12
  63. package/dist/template-names.js +13 -17
  64. package/package.json +5 -4
  65. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  66. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  67. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  68. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  69. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  70. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  71. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  72. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  73. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  74. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  75. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  76. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  77. package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
  78. package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
  79. package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
  80. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  81. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  82. package/site-templates/designs/minimalism/sample/README.md +48 -0
  83. package/site-templates/designs/standard/DESIGN.md +103 -6
  84. package/site-templates/marketing-site/DESIGN.md +103 -6
  85. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
  86. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
  87. package/site-templates/marketing-site-free/DESIGN.md +103 -6
  88. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
  89. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
  90. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  91. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  92. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  93. package/site-templates/saas-product/DESIGN.md +103 -6
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
  97. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  98. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  99. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  100. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  101. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  102. package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  103. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  104. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  105. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  106. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  107. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  108. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
  110. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  111. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  112. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  113. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  114. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  115. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  116. package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
  117. package/site-templates/shapes/organization-service/provenance.json +4 -0
  118. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
  119. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
  120. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  121. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  122. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  123. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  124. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  125. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
  126. package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
  127. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  128. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  129. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  130. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  131. package/site-templates/shapes/person-service/provenance.json +4 -0
  132. package/templates/.claude/rules/architecture.md +7 -0
  133. package/templates/.claude/skills/README.md +5 -3
  134. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  135. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  136. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  137. package/templates/.claude/skills/add-page/references/page-types.md +85 -72
  138. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  139. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  140. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  141. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  142. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  143. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  144. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  145. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  146. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  147. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  148. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  149. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  150. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
  151. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  152. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  153. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  154. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  155. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  156. package/templates/.github/workflows/ci.yml +4 -0
  157. package/templates/.prettierignore +1 -0
  158. package/templates/CLAUDE.md +146 -103
  159. package/templates/DESIGN.md +103 -6
  160. package/templates/README.md +63 -43
  161. package/templates/apps/api/.env.example +2 -1
  162. package/templates/apps/api/CLAUDE.md +3 -2
  163. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  164. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  165. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  166. package/templates/apps/api/src/config/index.ts +35 -16
  167. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  168. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  169. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  170. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  171. package/templates/apps/api/src/lib/stripe.ts +9 -0
  172. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  173. package/templates/apps/api/src/middleware/auth.ts +54 -0
  174. package/templates/apps/api/src/openapi/spec.ts +63 -12
  175. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  176. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  177. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  178. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  179. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  180. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  181. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  182. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  183. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  184. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  185. package/templates/apps/api/src/server.ts +35 -0
  186. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  187. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  188. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  189. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  190. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  191. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  192. package/templates/apps/api/src/services/revalidation.ts +60 -15
  193. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  194. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  197. package/templates/apps/web/.env.example +13 -0
  198. package/templates/apps/web/CLAUDE.md +22 -28
  199. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  200. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  201. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  202. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  203. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  205. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  206. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  207. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  208. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  209. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  210. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  211. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  212. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  213. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  214. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  215. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  216. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  217. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  218. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
  219. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  220. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  221. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  222. package/templates/apps/web/app/[locale]/layout.tsx +65 -17
  223. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  224. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  225. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  226. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  227. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  228. package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
  229. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  230. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  232. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  233. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  234. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  235. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  236. package/templates/apps/web/lib/locale-guard.ts +18 -0
  237. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  238. package/templates/apps/web/lib/nav-config.ts +50 -37
  239. package/templates/apps/web/lib/site-brand.tsx +13 -9
  240. package/templates/apps/web/lib/site-identity.ts +14 -0
  241. package/templates/apps/web/lib/site-logo.tsx +36 -0
  242. package/templates/apps/web/lib/site-theme.ts +46 -26
  243. package/templates/apps/web/messages/en.json +45 -5
  244. package/templates/apps/web/middleware.ts +28 -4
  245. package/templates/content/_site.mdx +4 -2
  246. package/templates/content/en/BlogList/blog.mdx +37 -16
  247. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  248. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  249. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  250. package/templates/content/en/BookCall/start-project.mdx +56 -0
  251. package/templates/content/en/ContactPage/contact.mdx +43 -34
  252. package/templates/content/en/HomePage/home.mdx +10 -22
  253. package/templates/content/en/LandingPage/meet.mdx +59 -0
  254. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  255. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  256. package/templates/database/CHANGELOG.md +14 -0
  257. package/templates/database/package.json +1 -1
  258. package/templates/package.json +2 -1
  259. package/templates/tools/check-claude-md.ts +128 -0
  260. package/versions.json +33 -33
  261. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
  262. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  263. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  264. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  265. package/site-templates/minimalism-site/DESIGN.md +0 -162
  266. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
  267. package/site-templates/minimalism-site/ailk-template.json +0 -23
  268. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  269. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
  270. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  271. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  272. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  273. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  274. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  275. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  276. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  277. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  278. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  279. package/site-templates/minimalism-site/provenance.json +0 -4
  280. package/site-templates/simple-service/DESIGN.md +0 -162
  281. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  282. package/site-templates/simple-service/ailk-template.json +0 -11
  283. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  284. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  285. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  286. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  287. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  288. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  289. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  290. package/site-templates/simple-service/provenance.json +0 -4
  291. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  292. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  293. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  294. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  295. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
  296. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  297. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  298. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  299. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  300. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  301. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  302. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  303. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  304. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  305. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  306. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  307. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
  308. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
  309. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
  310. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
@@ -0,0 +1,78 @@
1
+ /** One row of theme's contrast audit, as the audit script prints it. */
2
+ export interface ContrastRow {
3
+ name: string;
4
+ bg: string;
5
+ fg: string;
6
+ ratio: number | null;
7
+ threshold: number;
8
+ status: "pass" | "fail" | "unmeasurable";
9
+ }
10
+ export type DesignContrastVerdict = {
11
+ ok: true;
12
+ pairs: number;
13
+ } | {
14
+ ok: false;
15
+ pairs: number;
16
+ failing: ContrastRow[];
17
+ reason: string;
18
+ };
19
+ /**
20
+ * Decide whether `design`'s derived token set passes WCAG AA. Every row whose
21
+ * status is not `pass` fails it; an audit of no pair never passes.
22
+ */
23
+ export declare function assessDesignContrast(design: string, results: ContrastRow[]): DesignContrastVerdict;
24
+ /** `__tests__/fixtures/page-jsonld.ts`'s stdout: page path → its composed JSON-LD. */
25
+ export type ComposedPages = Record<string, {
26
+ jsonld: string;
27
+ sectionsValid: boolean;
28
+ }>;
29
+ /** One changed node: the page and its JSON-pointer path (or a `(page …)` pseudo-path). */
30
+ export interface DriftNode {
31
+ page: string;
32
+ path: string;
33
+ }
34
+ /**
35
+ * Every node that differs between two compositions, sorted by page then path.
36
+ * A removed or added page, and a page whose sections no longer validate after,
37
+ * are one pseudo-path node each; a `logo` subtree is never compared.
38
+ */
39
+ export declare function jsonLdDrift(before: ComposedPages, after: ComposedPages): DriftNode[];
40
+ export type JsonLdDriftVerdict = {
41
+ ok: true;
42
+ pages: number;
43
+ } | {
44
+ ok: false;
45
+ pages: number;
46
+ changed: DriftNode[];
47
+ reason: string;
48
+ };
49
+ /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
50
+ export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
51
+ export interface MeasureApplyDesignDriftOptions {
52
+ /** create-ailk's built `dist/cli.js`. */
53
+ cliPath: string;
54
+ /** The monorepo's `tsx` binary. */
55
+ tsxPath: string;
56
+ /** `__tests__/fixtures/page-jsonld.ts` — the one production composer, run from source. */
57
+ composerPath: string;
58
+ /** Its tsconfig, passed as `TSX_TSCONFIG_PATH`. */
59
+ composerTsconfig: string;
60
+ /** The design `apply-design` applies (a name or a package path). */
61
+ design: string;
62
+ /** An existing empty directory; the drift site is scaffolded at `<workDir>/site`. */
63
+ workDir: string;
64
+ /** The loopback registry the CLI resolves modules and templates from. */
65
+ registryUrl?: string;
66
+ /** Written to the site's own `ailk.key`, which `apply-design` gates a non-Free design on. */
67
+ licenseKey?: string;
68
+ }
69
+ /**
70
+ * Scaffold a keyless `organization-product` site on `standard`, compose its
71
+ * JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
72
+ * exit at any step throws naming the step and the child's stderr.
73
+ */
74
+ export declare function measureApplyDesignDrift(opts: MeasureApplyDesignDriftOptions): {
75
+ before: ComposedPages;
76
+ after: ComposedPages;
77
+ applyStdout: string;
78
+ };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The two design checks a design-wearing run of `scripts/verify-scaffold.mjs`
3
+ * runs before install (#6823, spec
4
+ * docs/specs/6823-the-matrix-runs-the-derived-spec.md D3).
5
+ *
6
+ * CONTRAST — `assessDesignContrast` turns the rows
7
+ * `packages/theme/scripts/audit-site-theme-contrast.ts` prints (theme's
8
+ * `auditTokenSetContrast` over the design's derived token set) into a verdict
9
+ * naming every non-passing `{bg, fg}` pair and the design. The WCAG math and
10
+ * the pair list stay theme's; this file only decides pass/fail.
11
+ *
12
+ * DRIFT — `jsonLdDrift` / `assessJsonLdDrift` compare every page's composed
13
+ * JSON-LD before and after `apply-design`, excluding the logo image (the PRD
14
+ * metric "structural drift after apply"), and `measureApplyDesignDrift`
15
+ * gathers the two snapshots (the `renderRoute` precedent, #6821 D4): it
16
+ * touches the filesystem and processes only through the paths it is handed,
17
+ * so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
18
+ * procedure. Nothing here imports `@working-theory/theme`.
19
+ */
20
+ import { execFileSync } from "node:child_process";
21
+ import { writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ function describePair(row) {
24
+ const measured = row.status === "unmeasurable" || row.ratio === null
25
+ ? "unmeasurable"
26
+ : `${row.ratio.toFixed(2)}:1 < ${row.threshold}:1`;
27
+ return `${row.name} {bg ${row.bg}, fg ${row.fg}} ${measured}`;
28
+ }
29
+ /**
30
+ * Decide whether `design`'s derived token set passes WCAG AA. Every row whose
31
+ * status is not `pass` fails it; an audit of no pair never passes.
32
+ */
33
+ export function assessDesignContrast(design, results) {
34
+ const pairs = results.length;
35
+ if (pairs === 0) {
36
+ return { ok: false, pairs, failing: [], reason: `design ${design}: no contrast pair was audited` };
37
+ }
38
+ const failing = results.filter((row) => row.status !== "pass");
39
+ if (failing.length === 0)
40
+ return { ok: true, pairs };
41
+ return {
42
+ ok: false,
43
+ pairs,
44
+ failing,
45
+ reason: `design ${design}: ${failing.length} of ${pairs} contrast pair(s) below WCAG AA — ${failing
46
+ .map(describePair)
47
+ .join("; ")}`,
48
+ };
49
+ }
50
+ /** schema.org's logo property — the one subtree the PRD's "excluding the logo image" skips. */
51
+ const LOGO_KEY = "logo";
52
+ const kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
53
+ function walk(before, after, path, out) {
54
+ const kind = kindOf(before);
55
+ if (kind !== kindOf(after)) {
56
+ out.push(path);
57
+ return;
58
+ }
59
+ if (kind === "array") {
60
+ const a = before;
61
+ const b = after;
62
+ if (a.length !== b.length)
63
+ out.push(path);
64
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
65
+ walk(a[i], b[i], `${path}/${i}`, out);
66
+ return;
67
+ }
68
+ if (kind === "object") {
69
+ const a = before;
70
+ const b = after;
71
+ for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
72
+ if (key === LOGO_KEY)
73
+ continue;
74
+ walk(a[key], b[key], `${path}/${key}`, out);
75
+ }
76
+ return;
77
+ }
78
+ if (before !== after)
79
+ out.push(path);
80
+ }
81
+ /**
82
+ * Every node that differs between two compositions, sorted by page then path.
83
+ * A removed or added page, and a page whose sections no longer validate after,
84
+ * are one pseudo-path node each; a `logo` subtree is never compared.
85
+ */
86
+ export function jsonLdDrift(before, after) {
87
+ const nodes = [];
88
+ for (const page of [...new Set([...Object.keys(before), ...Object.keys(after)])]) {
89
+ const b = before[page];
90
+ const a = after[page];
91
+ if (a === undefined) {
92
+ nodes.push({ page, path: "(page removed)" });
93
+ }
94
+ else if (b === undefined) {
95
+ nodes.push({ page, path: "(page added)" });
96
+ }
97
+ else if (!a.sectionsValid) {
98
+ nodes.push({ page, path: "(sections invalid)" });
99
+ }
100
+ else {
101
+ const paths = [];
102
+ walk(JSON.parse(b.jsonld), JSON.parse(a.jsonld), "", paths);
103
+ for (const path of paths)
104
+ nodes.push({ page, path: path === "" ? "/" : path });
105
+ }
106
+ }
107
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
108
+ return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
109
+ }
110
+ /** How many changed nodes a drift reason lists before it stops. */
111
+ const REASON_NODE_LIMIT = 10;
112
+ /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
113
+ export function assessJsonLdDrift(design, before, after) {
114
+ const pages = Object.keys(before).length;
115
+ if (pages === 0) {
116
+ return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
117
+ }
118
+ const changed = jsonLdDrift(before, after);
119
+ if (changed.length === 0)
120
+ return { ok: true, pages };
121
+ const listed = changed
122
+ .slice(0, REASON_NODE_LIMIT)
123
+ .map((n) => `${n.page} ${n.path}`)
124
+ .join("; ");
125
+ return {
126
+ ok: false,
127
+ pages,
128
+ changed,
129
+ reason: `design ${design}: ${changed.length} JSON-LD node(s) changed by apply-design — ${listed}${changed.length > REASON_NODE_LIMIT ? "; …" : ""}`,
130
+ };
131
+ }
132
+ /**
133
+ * Scaffold a keyless `organization-product` site on `standard`, compose its
134
+ * JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
135
+ * exit at any step throws naming the step and the child's stderr.
136
+ */
137
+ export function measureApplyDesignDrift(opts) {
138
+ const baseEnv = { ...process.env };
139
+ delete baseEnv["AILK_LICENSE_KEY"];
140
+ const registryEnv = opts.registryUrl === undefined
141
+ ? {}
142
+ : { AILK_MODULE_REGISTRY_URL: opts.registryUrl, AILK_TEMPLATE_REGISTRY_URL: opts.registryUrl };
143
+ const site = join(opts.workDir, "site");
144
+ const exec = (label, cmd, args, env) => {
145
+ try {
146
+ return execFileSync(cmd, args, {
147
+ encoding: "utf8",
148
+ env: { ...baseEnv, ...env },
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ maxBuffer: 64 * 1024 * 1024,
151
+ });
152
+ }
153
+ catch (err) {
154
+ const { stderr, message } = err;
155
+ throw new Error(`drift: ${label} failed: ${(stderr || message || "").trim()}`, { cause: err });
156
+ }
157
+ };
158
+ const compose = (label) => {
159
+ const stdout = exec(label, opts.tsxPath, [opts.composerPath, site], { TSX_TSCONFIG_PATH: opts.composerTsconfig });
160
+ try {
161
+ return JSON.parse(stdout);
162
+ }
163
+ catch (err) {
164
+ throw new Error(`drift: ${label} printed no JSON: ${err.message}`, { cause: err });
165
+ }
166
+ };
167
+ exec("scaffold organization-product on standard", process.execPath, [opts.cliPath, site, "--shape", "organization-product", "--design", "standard"], registryEnv);
168
+ if (opts.licenseKey !== undefined)
169
+ writeFileSync(join(site, "ailk.key"), `${opts.licenseKey}\n`);
170
+ const before = compose("compose JSON-LD before apply-design");
171
+ const applyStdout = exec(`apply-design ${opts.design}`, process.execPath, [opts.cliPath, "apply-design", opts.design, "--target", site], opts.registryUrl === undefined ? {} : { AILK_MODULE_REGISTRY_URL: opts.registryUrl });
172
+ const after = compose("compose JSON-LD after apply-design");
173
+ return { before, after, applyStdout };
174
+ }
@@ -655,6 +655,11 @@ export function installBundleTree(options) {
655
655
  * path-safety check is needed here — `srcRoot` is our own staging tree, not
656
656
  * attacker-controlled input.
657
657
  *
658
+ * A directory is created only as the parent of a file actually copied, never
659
+ * for its own sake: a source directory whose every file is excluded (mapped
660
+ * away) leaves nothing behind at its original path (#6787 — an empty root
661
+ * `content/en/blog/` reads as an unregistered page type).
662
+ *
658
663
  * `markers`, when given, is substituted into every TEXT file on the way
659
664
  * through (#6460) — see `InstallBundleTreeOptions.markers`.
660
665
  */
@@ -669,7 +674,6 @@ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
669
674
  const rel = relative(srcRoot, srcPath);
670
675
  const destPath = join(destRoot, rel);
671
676
  if (entry.isDirectory()) {
672
- mkdirSync(destPath, { recursive: true });
673
677
  stack.push(srcPath);
674
678
  }
675
679
  else if (entry.isFile()) {
@@ -6,6 +6,15 @@ export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
6
6
  * describes the rule, never the value.
7
7
  */
8
8
  export declare function assertLicenseKeyShape(key: string): void;
9
+ /**
10
+ * Write `content` only when it differs, with mode 0600 either way. An existing
11
+ * file is tightened BEFORE the write — `mode` applies only on creation, so a
12
+ * looser pre-existing `.env` would otherwise hold the key for a moment at its
13
+ * old permissions.
14
+ */
15
+ export declare function writeOwnerOnly(path: string, content: string): void;
16
+ /** `text` plus `line`, starting on a fresh line. */
17
+ export declare function appendLine(text: string, line: string): string;
9
18
  /**
10
19
  * Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
11
20
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
@@ -48,7 +48,7 @@ export function assertLicenseKeyShape(key) {
48
48
  * looser pre-existing `.env` would otherwise hold the key for a moment at its
49
49
  * old permissions.
50
50
  */
51
- function writeOwnerOnly(path, content) {
51
+ export function writeOwnerOnly(path, content) {
52
52
  mkdirSync(dirname(path), { recursive: true });
53
53
  if (existsSync(path))
54
54
  chmodSync(path, 0o600);
@@ -58,7 +58,7 @@ function writeOwnerOnly(path, content) {
58
58
  chmodSync(path, 0o600);
59
59
  }
60
60
  /** `text` plus `line`, starting on a fresh line. */
61
- function appendLine(text, line) {
61
+ export function appendLine(text, line) {
62
62
  const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
63
63
  return `${text}${sep}${line}\n`;
64
64
  }
@@ -16,8 +16,13 @@
16
16
  * `resolvePaidModules` returns immediately, before touching `process.env` or
17
17
  * the network, so a bare/all-Free scaffold is provably unaffected by this
18
18
  * gate (K1's regression floor).
19
+ *
20
+ * Also carries the design gate (#6760): `assertDesignAllowed` refuses a
21
+ * non-`standard` design at the `free` level, naming the design and the
22
+ * run's level, before `scaffold()`'s first disk write.
19
23
  */
20
24
  import type { ModuleName } from "../parse-args.js";
25
+ import { type Level } from "./module-tier.js";
21
26
  /**
22
27
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
23
28
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -33,6 +38,29 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
33
38
  export declare class LicenseRequiredError extends Error {
34
39
  constructor(message: string);
35
40
  }
41
+ /**
42
+ * Design names every level renders (#6760 D1). Anything else in
43
+ * `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
44
+ * `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
45
+ * `assertDesignAllowed`'s `free` parameter carries that verdict in.
46
+ */
47
+ export declare const FREE_DESIGNS: readonly string[];
48
+ /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
49
+ export declare const DESIGN_LEVELS: readonly Level[];
50
+ /**
51
+ * Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
52
+ * path design #6813 D7). Pure and synchronous — takes the level
53
+ * `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
54
+ * derives a level itself (D2, fail-closed: both checks below are allow-lists,
55
+ * so an unrecognized `level` refuses rather than granting it).
56
+ *
57
+ * @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
58
+ * (a design NAME's own answer), so a caller passing a name never changes
59
+ * behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
60
+ * identity rule) passes it explicitly: a byte-identical copy of a shipped
61
+ * Free design is Free; any other path is not, whatever it is named.
62
+ */
63
+ export declare function assertDesignAllowed(design: string | undefined, level: Level, free?: boolean): void;
36
64
  export interface ResolvePaidModulesOptions {
37
65
  /** The requested module set's paid subset (see `module-tier.ts`). */
38
66
  paidModules: ModuleName[];
@@ -52,7 +80,20 @@ export interface ResolvePaidModulesOptions {
52
80
  * to do. Absent ⟹ a plain `--modules` run, whose messages are unchanged.
53
81
  */
54
82
  templateName?: string;
83
+ /**
84
+ * The key's derived level (#6759 D5), when one was derived on this call's
85
+ * path. Absent on the `--modules`-keep branch, where no level is derived
86
+ * (NFR-2) — the refusal then names only the module and its own level,
87
+ * never the caller's.
88
+ */
89
+ level?: Level;
55
90
  }
91
+ /**
92
+ * The message phrase for one module (D5): what level includes it, or that it
93
+ * is an add-on. Pure — derived from `levelOf`, never from the current run's
94
+ * own derived level.
95
+ */
96
+ export declare function levelPhrase(m: ModuleName): string;
56
97
  /**
57
98
  * Resolve every requested paid module to its fetched bundle bytes, or throw
58
99
  * `LicenseRequiredError` naming exactly why the run cannot proceed. Never
@@ -61,3 +102,51 @@ export interface ResolvePaidModulesOptions {
61
102
  * BEFORE the scaffold copy — see `programmatic.ts`'s `scaffold()`).
62
103
  */
63
104
  export declare function resolvePaidModules(options: ResolvePaidModulesOptions): Promise<Map<ModuleName, Uint8Array>>;
105
+ /** Inputs shared by `deriveLevel` and `resolveModuleSet` (D4/D5). */
106
+ export interface GateOptions {
107
+ /** Defaults to `process.env["AILK_LICENSE_KEY"]`. Injectable for tests. */
108
+ licenseKey?: string | undefined;
109
+ /** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
110
+ registryUrl?: string;
111
+ /** Forwarded to `fetchModule`; defaults to this package's own version. */
112
+ version?: string;
113
+ /** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
114
+ fetchImpl?: typeof fetch;
115
+ }
116
+ /**
117
+ * Derive the key's level from what the registry actually answers (D4) — the
118
+ * level only chooses which modules to ASK FOR; every byte is still
119
+ * authorized by the registry per module. Nothing client-side is an
120
+ * entitlement claim: the JWT is never parsed here.
121
+ *
122
+ * No key ⟹ `"free"`, zero network. A present key is shape-checked first,
123
+ * whichever source it came from (SEC-2). Then: probe every paid member of
124
+ * `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
125
+ * (a key that does not unlock all of Pro is not silently downgraded); any
126
+ * other failure (404, 5xx, unreachable) is wrapped so the caller sees it
127
+ * could not determine the level, and is told the escape hatch (`--modules`).
128
+ * Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
129
+ * there is the expected "this key is Pro, not Pro Plus" answer, not a
130
+ * refusal.
131
+ */
132
+ export declare function deriveLevel(options: GateOptions): Promise<{
133
+ level: Level;
134
+ bundles: Map<ModuleName, Uint8Array>;
135
+ }>;
136
+ /**
137
+ * The one entry point that turns `--modules` + the key into the effective
138
+ * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
139
+ * calls this for every run except a named `--template` alias (NFR-4).
140
+ *
141
+ * A named list with a non-addition (the KEEP branch, D2) resolves it exactly
142
+ * as today, with no level derived and no extra request (NFR-2). Otherwise the
143
+ * level is derived first, the set composed from it, and any named addition
144
+ * not already fetched during the level probe is resolved on top.
145
+ */
146
+ export declare function resolveModuleSet(options: GateOptions & {
147
+ named?: readonly ModuleName[] | undefined;
148
+ }): Promise<{
149
+ modules: ModuleName[];
150
+ bundles: Map<ModuleName, Uint8Array>;
151
+ level?: Level;
152
+ }>;
@@ -1,4 +1,6 @@
1
1
  import { FetchModuleError, fetchModule } from "./fetch-module.js";
2
+ import { assertLicenseKeyShape } from "./license-key-files.js";
3
+ import { ADD_ONS, LEVEL_LABEL, LEVEL_MODULES, OPT_IN_MODULES, PAID_MODULES, classifyModules, composeModuleSet, levelOf, } from "./module-tier.js";
2
4
  /**
3
5
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
4
6
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -17,6 +19,64 @@ export class LicenseRequiredError extends Error {
17
19
  this.name = "LicenseRequiredError";
18
20
  }
19
21
  }
22
+ /**
23
+ * Design names every level renders (#6760 D1). Anything else in
24
+ * `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
25
+ * `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
26
+ * `assertDesignAllowed`'s `free` parameter carries that verdict in.
27
+ */
28
+ export const FREE_DESIGNS = ["standard"];
29
+ /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
30
+ export const DESIGN_LEVELS = ["pro", "pro-plus"];
31
+ /**
32
+ * Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
33
+ * path design #6813 D7). Pure and synchronous — takes the level
34
+ * `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
35
+ * derives a level itself (D2, fail-closed: both checks below are allow-lists,
36
+ * so an unrecognized `level` refuses rather than granting it).
37
+ *
38
+ * @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
39
+ * (a design NAME's own answer), so a caller passing a name never changes
40
+ * behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
41
+ * identity rule) passes it explicitly: a byte-identical copy of a shipped
42
+ * Free design is Free; any other path is not, whatever it is named.
43
+ */
44
+ export function assertDesignAllowed(design, level, free = design !== undefined && FREE_DESIGNS.includes(design)) {
45
+ if (design === undefined || free)
46
+ return;
47
+ if (DESIGN_LEVELS.includes(level))
48
+ return;
49
+ throw new LicenseRequiredError(`design "${design}" is a Pro feature — this run's licence level is ${LEVEL_LABEL[level]}; pass --license <key> or set AILK_LICENSE_KEY with a Pro or Pro Plus key → ${CONVERSION_HINT_URL}`);
50
+ }
51
+ /**
52
+ * The message phrase for one module (D5): what level includes it, or that it
53
+ * is an add-on. Pure — derived from `levelOf`, never from the current run's
54
+ * own derived level.
55
+ */
56
+ export function levelPhrase(m) {
57
+ const level = levelOf(m);
58
+ if (level === "add-on") {
59
+ return `"${m}" is an add-on, bought on top of Pro or Pro Plus`;
60
+ }
61
+ if (level === "free") {
62
+ return `"${m}" is included at ${LEVEL_LABEL.free}`;
63
+ }
64
+ return `"${m}" is included at ${LEVEL_LABEL[level]}`;
65
+ }
66
+ /** M1 / M1t (D5): the no-key refusal, with or without a named template. */
67
+ function noKeyMessage(paidModules, templateName) {
68
+ const phrases = paidModules.map((m) => levelPhrase(m)).join("; ");
69
+ const intro = templateName !== undefined
70
+ ? `template "${templateName}" needs a licence: ${phrases}`
71
+ : `no licence key, so this is a Free scaffold: ${phrases}`;
72
+ return `${intro} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
73
+ }
74
+ /** M2 (D5): a key the registry rejected for one module. */
75
+ function licenseErrorMessage(err, moduleName, templateName, level) {
76
+ const templateSuffix = templateName !== undefined ? ` (required by template "${templateName}")` : "";
77
+ const levelSuffix = level !== undefined ? ` — your licence is at ${LEVEL_LABEL[level]}` : "";
78
+ return `${err.message}; ${levelPhrase(moduleName)}${templateSuffix}${levelSuffix} → ${CONVERSION_HINT_URL}`;
79
+ }
20
80
  /**
21
81
  * Resolve every requested paid module to its fetched bundle bytes, or throw
22
82
  * `LicenseRequiredError` naming exactly why the run cannot proceed. Never
@@ -33,8 +93,11 @@ export async function resolvePaidModules(options) {
33
93
  }
34
94
  const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
35
95
  if (token === undefined || token.length === 0) {
36
- throw new LicenseRequiredError(conversionHint(paidModules, options.templateName));
96
+ throw new LicenseRequiredError(noKeyMessage(paidModules, options.templateName));
37
97
  }
98
+ // SEC-2 — the `--license` flag and the env var are validated alike,
99
+ // whichever source this token came from, before it reaches a request.
100
+ assertLicenseKeyShape(token);
38
101
  const bundles = new Map();
39
102
  for (const moduleName of paidModules) {
40
103
  try {
@@ -55,11 +118,9 @@ export async function resolvePaidModules(options) {
55
118
  if (err instanceof FetchModuleError) {
56
119
  if (err.isLicenseError) {
57
120
  // DD5 row 2: the mapped 401/403 already names the module the key
58
- // does not cover; the template is appended so the buyer can see
59
- // WHICH of the things they asked for needs it.
60
- throw new LicenseRequiredError(options.templateName !== undefined
61
- ? `${err.message}, required by template "${options.templateName}" → ${CONVERSION_HINT_URL}`
62
- : `${err.message} → ${CONVERSION_HINT_URL}`);
121
+ // does not cover; the template and level are appended so the buyer
122
+ // can see WHICH of the things they asked for needs it.
123
+ throw new LicenseRequiredError(licenseErrorMessage(err, moduleName, options.templateName, options.level));
63
124
  }
64
125
  // Not a license-gating signal (e.g. 404 "not yet available", or the
65
126
  // registry unreachable) — a generic failure, not the honest-denial
@@ -72,10 +133,108 @@ export async function resolvePaidModules(options) {
72
133
  }
73
134
  return bundles;
74
135
  }
75
- function conversionHint(paidModules, templateName) {
76
- const list = paidModules.join(", ");
77
- const requested = templateName !== undefined
78
- ? `template "${templateName}" requires Pro module(s) (${list})`
79
- : `Pro module(s) requested (${list})`;
80
- return `${requested} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
136
+ function forward(options) {
137
+ return {
138
+ ...(options.registryUrl !== undefined ? { registryUrl: options.registryUrl } : {}),
139
+ ...(options.version !== undefined ? { version: options.version } : {}),
140
+ ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
141
+ };
142
+ }
143
+ /**
144
+ * Derive the key's level from what the registry actually answers (D4) — the
145
+ * level only chooses which modules to ASK FOR; every byte is still
146
+ * authorized by the registry per module. Nothing client-side is an
147
+ * entitlement claim: the JWT is never parsed here.
148
+ *
149
+ * No key ⟹ `"free"`, zero network. A present key is shape-checked first,
150
+ * whichever source it came from (SEC-2). Then: probe every paid member of
151
+ * `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
152
+ * (a key that does not unlock all of Pro is not silently downgraded); any
153
+ * other failure (404, 5xx, unreachable) is wrapped so the caller sees it
154
+ * could not determine the level, and is told the escape hatch (`--modules`).
155
+ * Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
156
+ * there is the expected "this key is Pro, not Pro Plus" answer, not a
157
+ * refusal.
158
+ */
159
+ export async function deriveLevel(options) {
160
+ const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
161
+ if (token === undefined || token.length === 0) {
162
+ return { level: "free", bundles: new Map() };
163
+ }
164
+ assertLicenseKeyShape(token);
165
+ const bundles = new Map();
166
+ const proModules = LEVEL_MODULES.pro.filter((m) => PAID_MODULES.includes(m));
167
+ for (const moduleName of proModules) {
168
+ try {
169
+ const bytes = await fetchModule({ moduleName, token, ...forward(options) });
170
+ bundles.set(moduleName, bytes);
171
+ }
172
+ catch (err) {
173
+ if (err instanceof FetchModuleError) {
174
+ if (err.isLicenseError) {
175
+ throw new LicenseRequiredError(licenseErrorMessage(err, moduleName));
176
+ }
177
+ throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
178
+ }
179
+ throw err;
180
+ }
181
+ }
182
+ try {
183
+ const bytes = await fetchModule({
184
+ moduleName: "multi-tenant",
185
+ token,
186
+ ...forward(options),
187
+ });
188
+ bundles.set("multi-tenant", bytes);
189
+ return { level: "pro-plus", bundles };
190
+ }
191
+ catch (err) {
192
+ if (err instanceof FetchModuleError) {
193
+ if (err.isLicenseError) {
194
+ return { level: "pro", bundles };
195
+ }
196
+ throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
197
+ }
198
+ throw err;
199
+ }
200
+ }
201
+ /**
202
+ * The one entry point that turns `--modules` + the key into the effective
203
+ * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
204
+ * calls this for every run except a named `--template` alias (NFR-4).
205
+ *
206
+ * A named list with a non-addition (the KEEP branch, D2) resolves it exactly
207
+ * as today, with no level derived and no extra request (NFR-2). Otherwise the
208
+ * level is derived first, the set composed from it, and any named addition
209
+ * not already fetched during the level probe is resolved on top.
210
+ */
211
+ export async function resolveModuleSet(options) {
212
+ const namedList = options.named ?? [];
213
+ const additions = [...ADD_ONS, ...OPT_IN_MODULES];
214
+ const isKeep = namedList.some((m) => !additions.includes(m));
215
+ if (isKeep) {
216
+ const modules = composeModuleSet(namedList, "free");
217
+ const { paid } = classifyModules(modules);
218
+ const bundles = await resolvePaidModules({
219
+ paidModules: paid,
220
+ licenseKey: options.licenseKey,
221
+ ...forward(options),
222
+ });
223
+ return { modules, bundles };
224
+ }
225
+ const { level, bundles } = await deriveLevel(options);
226
+ const modules = composeModuleSet(namedList, level);
227
+ const { paid } = classifyModules(modules);
228
+ const missing = paid.filter((m) => !bundles.has(m));
229
+ if (missing.length > 0) {
230
+ const extra = await resolvePaidModules({
231
+ paidModules: missing,
232
+ licenseKey: options.licenseKey,
233
+ level,
234
+ ...forward(options),
235
+ });
236
+ for (const [k, v] of extra)
237
+ bundles.set(k, v);
238
+ }
239
+ return { modules, bundles, level };
81
240
  }