create-ailk 0.6.1 → 0.8.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 (324) hide show
  1. package/README.md +87 -14
  2. package/component-catalog.json +735 -95
  3. package/component-catalog.md +13 -7
  4. package/dist/add-module-core.d.ts +22 -0
  5. package/dist/add-module-core.js +50 -0
  6. package/dist/add-module.d.ts +89 -0
  7. package/dist/add-module.js +334 -0
  8. package/dist/category-profile.d.ts +126 -0
  9. package/dist/category-profile.js +262 -0
  10. package/dist/cli.js +89 -29
  11. package/dist/index.d.ts +4 -2
  12. package/dist/index.js +6 -1
  13. package/dist/lib/extract-module-bundle.d.ts +56 -1
  14. package/dist/lib/extract-module-bundle.js +170 -14
  15. package/dist/lib/license-key-files.d.ts +15 -0
  16. package/dist/lib/license-key-files.js +102 -0
  17. package/dist/lib/module-license-gate.d.ts +79 -0
  18. package/dist/lib/module-license-gate.js +160 -12
  19. package/dist/lib/module-tier.d.ts +45 -16
  20. package/dist/lib/module-tier.js +76 -0
  21. package/dist/lib/pro-callout.d.ts +16 -10
  22. package/dist/lib/pro-callout.js +34 -3
  23. package/dist/license-set.d.ts +34 -0
  24. package/dist/license-set.js +194 -0
  25. package/dist/module-architecture.d.ts +20 -0
  26. package/dist/module-architecture.js +77 -19
  27. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  28. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  29. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  30. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  31. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  32. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  33. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  34. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  35. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  36. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  37. package/dist/parse-args.d.ts +85 -10
  38. package/dist/parse-args.js +227 -15
  39. package/dist/programmatic.d.ts +81 -5
  40. package/dist/programmatic.js +291 -32
  41. package/dist/site-chrome.d.ts +18 -7
  42. package/dist/site-chrome.js +32 -14
  43. package/dist/site-roster.d.ts +127 -0
  44. package/dist/site-roster.js +499 -0
  45. package/dist/starter-grid.d.ts +216 -0
  46. package/dist/starter-grid.js +449 -0
  47. package/dist/surfaces.d.ts +20 -7
  48. package/dist/surfaces.js +204 -24
  49. package/dist/sync-routes.d.ts +9 -0
  50. package/dist/sync-routes.js +2 -2
  51. package/dist/template-manifest.d.ts +94 -19
  52. package/dist/template-manifest.js +170 -289
  53. package/dist/template-names.d.ts +14 -12
  54. package/dist/template-names.js +13 -17
  55. package/package.json +6 -4
  56. package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
  57. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  58. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  59. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  60. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  61. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
  62. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
  63. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  64. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  65. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  66. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  67. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  68. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  69. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
  70. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  71. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  72. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  73. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  74. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  75. package/site-templates/designs/standard/DESIGN.md +167 -0
  76. package/site-templates/marketing-site/DESIGN.md +59 -40
  77. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
  78. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
  79. package/site-templates/marketing-site/ailk-template.json +1 -0
  80. package/site-templates/marketing-site-free/DESIGN.md +59 -40
  81. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
  82. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
  83. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  84. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  85. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  86. package/site-templates/minimalism-site/DESIGN.md +54 -40
  87. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  88. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
  89. package/site-templates/minimalism-site/ailk-template.json +3 -2
  90. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
  91. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  92. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  93. package/site-templates/saas-product/DESIGN.md +59 -40
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -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/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  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 +362 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -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/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  117. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  118. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  119. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  120. package/site-templates/shapes/organization-service/provenance.json +4 -0
  121. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  122. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  123. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  124. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  125. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  126. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  127. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  128. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  129. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  130. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  131. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  132. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  133. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  134. package/site-templates/shapes/person-service/provenance.json +4 -0
  135. package/templates/.claude/settings.json +15 -0
  136. package/templates/.claude/skills/README.md +10 -9
  137. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  138. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  139. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  140. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  141. package/templates/.claude/skills/add-page/references/page-types.md +115 -0
  142. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  143. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  144. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  145. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  146. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  147. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  148. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  149. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  150. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  151. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  152. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  153. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
  154. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
  155. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
  156. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  157. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  158. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  159. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  160. package/templates/.github/workflows/ci.yml +5 -0
  161. package/templates/CLAUDE.md +8 -2
  162. package/templates/DESIGN.md +56 -44
  163. package/templates/README.md +116 -46
  164. package/templates/apps/api/.env.example +2 -1
  165. package/templates/apps/api/CLAUDE.md +3 -2
  166. package/templates/apps/api/package.json +1 -1
  167. package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
  168. package/templates/apps/api/src/config/index.ts +46 -14
  169. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  170. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  171. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  172. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  173. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  174. package/templates/apps/api/src/lib/site.ts +5 -3
  175. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  176. package/templates/apps/api/src/middleware/auth.ts +9 -0
  177. package/templates/apps/api/src/openapi/spec.ts +63 -12
  178. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  179. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  180. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  181. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  182. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  183. package/templates/apps/api/src/server.ts +13 -0
  184. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  185. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  186. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  187. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  188. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  189. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  190. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  191. package/templates/apps/mcp/package.json +2 -1
  192. package/templates/apps/mcp/src/index.ts +7 -0
  193. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  194. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/web/__tests__/setup.ts +18 -0
  197. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  198. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  199. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  200. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  201. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  202. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  203. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  205. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  206. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  207. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  208. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  209. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
  210. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
  211. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  212. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  213. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
  214. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  215. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  216. package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
  217. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  218. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  219. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  220. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  221. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  222. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  223. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  224. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  225. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  226. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  227. package/templates/apps/web/app/[locale]/layout.tsx +146 -52
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  229. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  230. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  231. package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
  232. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  233. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  234. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  235. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  236. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  237. package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
  238. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  239. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  240. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  241. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  242. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  243. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  244. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  245. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  246. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
  247. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  248. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  249. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  250. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  251. package/templates/apps/web/app/error-fallback.tsx +17 -45
  252. package/templates/apps/web/app/not-found.tsx +14 -38
  253. package/templates/apps/web/app/og/route.tsx +6 -0
  254. package/templates/apps/web/eslint.config.mjs +3 -0
  255. package/templates/apps/web/instrumentation.ts +14 -0
  256. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  257. package/templates/apps/web/lib/license-key-store.ts +52 -0
  258. package/templates/apps/web/lib/nav-config.ts +82 -18
  259. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  260. package/templates/apps/web/lib/site-brand.tsx +1 -1
  261. package/templates/apps/web/lib/site-chrome.ts +3 -3
  262. package/templates/apps/web/lib/site-identity.ts +148 -0
  263. package/templates/apps/web/lib/site-theme.ts +11 -0
  264. package/templates/apps/web/messages/en.json +78 -37
  265. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  266. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  267. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  268. package/templates/apps/web/public/favicon-16x16.png +0 -0
  269. package/templates/apps/web/public/favicon-32x32.png +0 -0
  270. package/templates/apps/web/public/favicon.ico +0 -0
  271. package/templates/apps/web/public/favicon.svg +3 -3
  272. package/templates/apps/web/public/logomark.svg +6 -3
  273. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  274. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  275. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  276. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  277. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  278. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  279. package/templates/apps/web/public/site.webmanifest +4 -4
  280. package/templates/apps/web/public/wordmark-light.svg +4 -0
  281. package/templates/apps/web/public/wordmark.svg +3 -3
  282. package/templates/apps/web/security-headers.config.ts +5 -1
  283. package/templates/content/_site.mdx +25 -0
  284. package/templates/content/en/AboutPage/about.mdx +150 -144
  285. package/templates/content/en/BlogList/blog.mdx +126 -0
  286. package/templates/content/en/ContactPage/contact.mdx +21 -92
  287. package/templates/content/en/FAQPage/faq.mdx +86 -57
  288. package/templates/content/en/HomePage/home.mdx +268 -217
  289. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
  290. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  291. package/templates/database/CHANGELOG.md +14 -0
  292. package/templates/database/package.json +1 -1
  293. package/templates/package.json +3 -1
  294. package/templates/tools/lint-architecture.ts +315 -0
  295. package/templates/tools/tsconfig.json +14 -0
  296. package/versions.json +33 -33
  297. package/site-templates/simple-service/DESIGN.md +0 -148
  298. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
  299. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
  300. package/site-templates/simple-service/ailk-template.json +0 -10
  301. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
  302. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  303. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  304. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  305. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  306. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  307. package/site-templates/simple-service/provenance.json +0 -4
  308. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  309. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  310. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  311. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  312. package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
  313. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  314. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  315. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  316. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
  317. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  318. package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
  319. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  320. package/templates/content/en/LandingPage/blog.mdx +0 -83
  321. package/templates/content/en/LandingPage/features.mdx +0 -103
  322. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  323. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  324. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -2,7 +2,12 @@
2
2
  * @file lead-routing.ts
3
3
  * @description Cross-namespace lead orchestration service (ADR 0009 D3).
4
4
  *
5
- * Pipeline: validate → enrich → persist (DB with file fallback) → email.
5
+ * Pipeline: validate → delivery guard → enrich → persist (DB with file
6
+ * fallback) → email. Where a lead may go is `resolveLeadDelivery()`
7
+ * (`lib/lead-delivery.ts`, #6730 D1): with no database but the email vars set,
8
+ * the owner email IS the delivery; in production with neither, nothing is
9
+ * accepted. A lead is reported sent only for a durable row or an email the
10
+ * provider accepted — never for the non-durable file fallback in production.
6
11
  * This is the first implementation of ADR 0009 D3's service-layer pattern.
7
12
  * Future cross-namespace orchestrators in apps/api/src/services/ should mirror
8
13
  * this shape: type-result returns, mocked-dep tests, non-transactional secondary
@@ -32,6 +37,7 @@ import { leadInputSchema } from "@working-theory/validation";
32
37
 
33
38
  import { writeLeadFallback } from "../lib/file-fallback.js";
34
39
  import { appendLeadRecord } from "../lib/lead-capture-store.js";
40
+ import { acceptsKind, resolveLeadDelivery } from "../lib/lead-delivery.js";
35
41
  import { prisma } from "../lib/prisma.js";
36
42
 
37
43
  import { enrichLead } from "./enrichment-shim.js";
@@ -65,11 +71,14 @@ export type SourceMeta = {
65
71
  receivedAt: string; // ISO
66
72
  };
67
73
 
74
+ /** Where the lead landed: a database row, the local file fallback, or the owner's inbox by email (#6730). */
75
+ export type Persisted = "db" | "file" | "email";
76
+
68
77
  export type RouteLeadResult =
69
78
  | {
70
79
  ok: true;
71
80
  id: string;
72
- persisted: "db" | "file";
81
+ persisted: Persisted;
73
82
  email:
74
83
  | { ok: true; messageId: string }
75
84
  | { ok: false; error: string }
@@ -78,7 +87,11 @@ export type RouteLeadResult =
78
87
  | {
79
88
  ok: false;
80
89
  error: {
81
- kind: "invalid_input" | "persist_failed" | "config_missing";
90
+ kind:
91
+ | "invalid_input"
92
+ | "persist_failed"
93
+ | "config_missing"
94
+ | "delivery_failed";
82
95
  message: string;
83
96
  };
84
97
  };
@@ -272,7 +285,7 @@ type EmailResult =
272
285
  async function sendEmail(
273
286
  input: RouteLeadInput,
274
287
  id: string,
275
- persisted: "db" | "file",
288
+ persisted: Persisted,
276
289
  ): Promise<EmailResult> {
277
290
  // Request-and-confirm SUPERSEDES this notification for a callback (#5970
278
291
  // D6). AC-1 requires the tenant to receive exactly one email, and the
@@ -316,7 +329,22 @@ async function sendEmail(
316
329
  siteId: input.siteId,
317
330
  source: input.kind === "callback" ? "callback" : input.source,
318
331
  email: input.kind === "callback" ? "" : input.email,
319
- payload: input.kind === "subscribe" ? {} : input.payload,
332
+ // A callback's contact details are top-level fields, not payload keys
333
+ // (#6730): with no database the email is the only record of the
334
+ // request, so it must carry the name and number to call back.
335
+ payload:
336
+ input.kind === "subscribe"
337
+ ? {}
338
+ : input.kind === "callback"
339
+ ? {
340
+ ...input.payload,
341
+ name: input.name,
342
+ phone: input.phone,
343
+ ...(input.preferredTime !== undefined && {
344
+ preferredTime: input.preferredTime,
345
+ }),
346
+ }
347
+ : input.payload,
320
348
  createdAt: new Date().toISOString(),
321
349
  };
322
350
 
@@ -324,6 +352,9 @@ async function sendEmail(
324
352
  lead: leadForEmail,
325
353
  recipientEmail: to,
326
354
  fromAddress: from,
355
+ // A callback has no visitor address to reply to; an empty Reply-To is
356
+ // not a valid header, so reply to the owner instead.
357
+ ...(input.kind === "callback" && { replyToOverride: to }),
327
358
  });
328
359
 
329
360
  if (emailResult.ok) {
@@ -337,6 +368,47 @@ async function sendEmail(
337
368
  return { ok: false, error: `${kind}: ${message}` };
338
369
  }
339
370
 
371
+ // ─── Email delivery (#6730 D2) ────────────────────────────────────────────────
372
+
373
+ type EmailDelivery =
374
+ | { ok: true; id: string; email: { ok: true; messageId: string } }
375
+ | { ok: false; message: string };
376
+
377
+ /**
378
+ * Deliver a lead as the owner email alone — success only when the provider
379
+ * accepted it. The id matches the file-fallback id format
380
+ * (`/^[a-z0-9_-]{8,64}$/i`).
381
+ */
382
+ async function deliverByEmail(input: RouteLeadInput): Promise<EmailDelivery> {
383
+ const id = `email-${Date.now()}-${randomUUID().slice(0, 8)}`;
384
+ const result = await sendEmail(input, id, "email");
385
+ if ("ok" in result && result.ok) return { ok: true, id, email: result };
386
+ return {
387
+ ok: false,
388
+ message: "error" in result ? result.error : `email skipped: ${result.reason}`,
389
+ };
390
+ }
391
+
392
+ // ─── Lead-capture record ─────────────────────────────────────────────────────
393
+
394
+ function captureRecordFor(input: RouteLeadInput, id: string, sourceMeta: SourceMeta) {
395
+ return {
396
+ id,
397
+ createdAt: sourceMeta.receivedAt,
398
+ source: input.kind === "callback" ? "callback" : input.source,
399
+ data:
400
+ input.kind === "callback"
401
+ ? { name: input.name, phone: input.phone, ...(input.payload ?? {}) }
402
+ : input.kind === "subscribe"
403
+ ? { email: input.email }
404
+ : { email: input.email, ...(input.payload ?? {}) },
405
+ metadata: {
406
+ ip: sourceMeta.ip,
407
+ userAgent: sourceMeta.userAgent,
408
+ },
409
+ };
410
+ }
411
+
340
412
  // ─── Main export ──────────────────────────────────────────────────────────────
341
413
 
342
414
  export async function routeLead(
@@ -352,13 +424,13 @@ export async function routeLead(
352
424
  };
353
425
  }
354
426
 
355
- // 2. Guard: if file capture is disabled AND no DB is configured, bail early.
356
- // Both paths are unavailable — returning a clear error is better than a
357
- // misleading 201 or a stack trace from Prisma on a missing DATABASE_URL.
358
- if (
359
- process.env.LEAD_CAPTURE_FILE_DISABLED === "true" &&
360
- !process.env.DATABASE_URL
361
- ) {
427
+ // 2. Guard: where can this lead go (#6730 D1)? A kind the configured
428
+ // delivery cannot take — anything in production with neither a database
429
+ // nor the email vars, a newsletter signup with no database — is refused
430
+ // before any write, so no visitor is told a message was sent when it
431
+ // reached nobody.
432
+ const delivery = resolveLeadDelivery();
433
+ if (!acceptsKind(delivery, input.kind)) {
362
434
  return {
363
435
  ok: false,
364
436
  error: {
@@ -374,9 +446,28 @@ export async function routeLead(
374
446
  // the real enrichment call with proper type alignment.
375
447
  await enrichLead(input as unknown as Parameters<typeof enrichLead>[0]);
376
448
 
449
+ // Email delivery — no database: the owner email is the delivery, so skip
450
+ // the DB and the (non-durable) file fallback entirely.
451
+ if (delivery === "email") {
452
+ const emailed = await deliverByEmail(input);
453
+ if (!emailed.ok) {
454
+ return {
455
+ ok: false,
456
+ error: { kind: "delivery_failed", message: emailed.message },
457
+ };
458
+ }
459
+ await appendLeadRecord(captureRecordFor(input, emailed.id, sourceMeta));
460
+ return {
461
+ ok: true,
462
+ id: emailed.id,
463
+ persisted: "email",
464
+ email: emailed.email,
465
+ };
466
+ }
467
+
377
468
  // 4. Persist primary (DB), fallback to file on error
378
469
  let id: string;
379
- let persisted: "db" | "file";
470
+ let persisted: Persisted;
380
471
  let skipEmail = false;
381
472
  let skipReason: string | undefined;
382
473
 
@@ -424,7 +515,41 @@ export async function routeLead(
424
515
  };
425
516
  }
426
517
 
427
- // 5. File fallback
518
+ // In production the file fallback is not durable (a serverless instance
519
+ // does not outlive the request), so reporting it as success would lose
520
+ // the lead silently. Deliver by email when that is configured; otherwise
521
+ // fail loudly (#6730 D2). A newsletter signup is never emailed — one
522
+ // owner email per address is not a list (`acceptsKind`).
523
+ if (process.env.NODE_ENV === "production") {
524
+ const dbMsg = dbErr instanceof Error ? dbErr.message : String(dbErr);
525
+ if (input.kind === "subscribe") {
526
+ return {
527
+ ok: false,
528
+ error: { kind: "persist_failed", message: `db: ${dbMsg}` },
529
+ };
530
+ }
531
+ const emailed = await deliverByEmail(input);
532
+ if (!emailed.ok) {
533
+ return {
534
+ ok: false,
535
+ error: {
536
+ kind: "persist_failed",
537
+ // Not the provider's text: it can echo an address, and this
538
+ // message is logged (spec D5).
539
+ message: `db: ${dbMsg}; email delivery failed`,
540
+ },
541
+ };
542
+ }
543
+ await appendLeadRecord(captureRecordFor(input, emailed.id, sourceMeta));
544
+ return {
545
+ ok: true,
546
+ id: emailed.id,
547
+ persisted: "email",
548
+ email: emailed.email,
549
+ };
550
+ }
551
+
552
+ // 5. File fallback (outside production only)
428
553
  // Include a UUID suffix so concurrent fallbacks produce unique IDs even
429
554
  // within the same millisecond. Format still matches /^[a-z0-9_-]{8,64}$/i.
430
555
  const fallbackLead = {
@@ -468,22 +593,7 @@ export async function routeLead(
468
593
  // 6. Write to lead-capture JSONL (always-on canonical AX surface).
469
594
  // Best-effort — a write failure does NOT roll back persistence or block email.
470
595
  // The file is the agent-readable surface; DB is the transactional record.
471
- const captureRecord = {
472
- id: id!,
473
- createdAt: sourceMeta.receivedAt,
474
- source: input.kind === "callback" ? "callback" : input.source,
475
- data:
476
- input.kind === "callback"
477
- ? { name: input.name, phone: input.phone, ...(input.payload ?? {}) }
478
- : input.kind === "subscribe"
479
- ? { email: input.email }
480
- : { email: input.email, ...(input.payload ?? {}) },
481
- metadata: {
482
- ip: sourceMeta.ip,
483
- userAgent: sourceMeta.userAgent,
484
- },
485
- };
486
- await appendLeadRecord(captureRecord);
596
+ await appendLeadRecord(captureRecordFor(input, id!, sourceMeta));
487
597
 
488
598
  // 7. Email notification (best-effort — never rolls back persistence)
489
599
  let emailResult: EmailResult;
@@ -25,10 +25,14 @@
25
25
  *
26
26
  * Two preconditions, neither of which this file can assert:
27
27
  *
28
- * 1. `resolveOwnerWorkspaces` must yield a workspace. On a build with no
29
- * `multi-tenant` module it yields none — see its own header, and #5677,
30
- * which made that a deliberate scope limit rather than a statement that
31
- * there is nothing to resolve.
28
+ * 1. The caller's scope must resolve to a workspace. With the
29
+ * `multi-tenant` module that is `resolveOwnerWorkspaces`. Without it
30
+ * (#6730 D7) — Free and Pro — that resolver yields none by design (its
31
+ * own header, #5677), so the scope is instead the default workspace,
32
+ * granted ONLY through `resolveDefaultTenantForOperator`
33
+ * (`default-tenant.ts`): `AILK_OPERATOR_EMAIL` set, the caller's
34
+ * VERIFIED email equal to it, and the deployment unambiguously
35
+ * one-workspace. Any other caller gets the empty page, never a wider one.
32
36
  * 2. The `siteId` the leads were WRITTEN under must equal a workspace id,
33
37
  * and there are TWO independent sources of it — both must agree with the
34
38
  * workspace, and neither is `currentSiteId()` on the primary path:
@@ -67,8 +71,10 @@
67
71
  import type { Prisma } from "@working-theory/database";
68
72
  import type { Id } from "@working-theory/ids";
69
73
 
74
+ import { isModuleEnabled } from "../config/modules.js";
70
75
  import { prisma } from "../lib/prisma.js";
71
76
 
77
+ import { resolveDefaultTenantForOperator } from "./default-tenant.js";
72
78
  import { resolveOwnerWorkspaces } from "./owner-workspaces.js";
73
79
 
74
80
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -178,6 +184,40 @@ export function readName(payload: Prisma.JsonValue): string | null {
178
184
  return typeof name === "string" ? name.slice(0, MAX_NAME_LENGTH) : null;
179
185
  }
180
186
 
187
+ // ─── Scope ────────────────────────────────────────────────────────────────────
188
+
189
+ /**
190
+ * The workspace ids whose leads the caller may read. With `multi-tenant`,
191
+ * every workspace they own or administer (`resolveOwnerWorkspaces`,
192
+ * unchanged). Without it (#6730 D7), the default workspace — only for the
193
+ * pinned, verified operator on a one-workspace deployment; `null` from the
194
+ * resolver means no scope at all.
195
+ */
196
+ async function resolveInboxSiteIds(
197
+ userId: Id<"user">,
198
+ ): Promise<{ ok: true; siteIds: string[] } | { ok: false; message: string }> {
199
+ if (!isModuleEnabled("multi-tenant")) {
200
+ try {
201
+ const tenant = await resolveDefaultTenantForOperator(userId);
202
+ return {
203
+ ok: true,
204
+ siteIds: tenant && LEAD_INBOX_ROLES.has(tenant.role) ? [tenant.workspaceId] : [],
205
+ };
206
+ } catch (err) {
207
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
208
+ }
209
+ }
210
+
211
+ const workspaces = await resolveOwnerWorkspaces(userId);
212
+ if (!workspaces.ok) return { ok: false, message: workspaces.error.message };
213
+ return {
214
+ ok: true,
215
+ siteIds: workspaces.value
216
+ .filter((workspace) => LEAD_INBOX_ROLES.has(workspace.role))
217
+ .map((workspace) => workspace.id),
218
+ };
219
+ }
220
+
181
221
  // ─── Main export ──────────────────────────────────────────────────────────────
182
222
 
183
223
  /**
@@ -199,17 +239,14 @@ export async function listOwnedLeads(params: {
199
239
  cursor?: string | undefined;
200
240
  limit: number;
201
241
  }): Promise<ListLeadsResult> {
202
- const workspaces = await resolveOwnerWorkspaces(params.userId);
203
- if (!workspaces.ok) {
242
+ const scope = await resolveInboxSiteIds(params.userId);
243
+ if (!scope.ok) {
204
244
  return {
205
245
  ok: false,
206
- error: { kind: "internal_error", message: workspaces.error.message },
246
+ error: { kind: "internal_error", message: scope.message },
207
247
  };
208
248
  }
209
-
210
- const siteIds = workspaces.value
211
- .filter((workspace) => LEAD_INBOX_ROLES.has(workspace.role))
212
- .map((workspace) => workspace.id);
249
+ const siteIds = scope.siteIds;
213
250
 
214
251
  // No workspace the caller owns or administers means no lead can match, so
215
252
  // there is nothing to query. An empty page is the honest answer — the same
@@ -15,9 +15,17 @@
15
15
  * - No fetch() call (structural test)
16
16
  */
17
17
 
18
- import { readFileSync } from "node:fs";
18
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
19
20
  import { resolve, join } from "node:path";
20
21
 
22
+ import {
23
+ entityMarkup,
24
+ runAudit,
25
+ schemaCoverage,
26
+ type AuditReport,
27
+ } from "@working-theory/aeo";
28
+
21
29
  import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
22
30
 
23
31
  const REPO_ROOT = resolve(__dirname, "../../..");
@@ -211,3 +219,69 @@ describe("structural guard — no fetch()", () => {
211
219
  expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
212
220
  });
213
221
  });
222
+
223
+ describe("buildSinglePageAuditInput — sitewide identity (#6597 AC-7)", () => {
224
+ const evidenceOf = (report: AuditReport, id: string) =>
225
+ report.perRule.find((r) => r.id === id);
226
+ const auditOf = async (input: { slug: string; type?: "AboutPage" }, root: string) =>
227
+ runAudit(await buildSinglePageAuditInput(input, root), [schemaCoverage, entityMarkup]);
228
+
229
+ it("schema-coverage passes for the monorepo's AboutPage/about", async () => {
230
+ const report = await auditOf({ slug: "about", type: "AboutPage" }, CONTENT_ROOT);
231
+ expect(evidenceOf(report, "schema-coverage")?.pass).toBe(true);
232
+ });
233
+
234
+ it("the monorepo home finds the identity: Identity (organization) ✓", async () => {
235
+ const report = await auditOf({ slug: "home" }, CONTENT_ROOT);
236
+ expect(evidenceOf(report, "entity-markup")?.evidence).toContain(
237
+ "Identity (organization) ✓",
238
+ );
239
+ });
240
+
241
+ it("stores the flat node array, the page's own primary first", async () => {
242
+ const input = await buildSinglePageAuditInput({ slug: "about", type: "AboutPage" }, CONTENT_ROOT);
243
+ const nodes = input.jsonLdByPage["AboutPage/about"] as Array<Record<string, unknown>>;
244
+ expect(nodes[0]!["@type"]).toBe("AboutPage");
245
+ expect(nodes.map((n) => n["@type"])).toEqual(
246
+ expect.arrayContaining(["Organization", "WebSite"]),
247
+ );
248
+ });
249
+
250
+ describe("a Person site", () => {
251
+ let root: string;
252
+ beforeAll(() => {
253
+ root = mkdtempSync(join(tmpdir(), "mcp-6597-"));
254
+ writeFileSync(
255
+ join(root, "_site.mdx"),
256
+ [
257
+ "---",
258
+ 'version: "1"',
259
+ "siteName: Ada",
260
+ "baseUrl: https://example.com",
261
+ "defaultLocale: en",
262
+ "siteIdentity:",
263
+ " kind: Person",
264
+ " name: Ada Lovelace",
265
+ " url: https://example.com",
266
+ "---",
267
+ "",
268
+ ].join("\n"),
269
+ );
270
+ mkdirSync(join(root, "en", "HomePage"), { recursive: true });
271
+ writeFileSync(
272
+ join(root, "en", "HomePage", "home.mdx"),
273
+ ["---", "type: HomePage", "slug: home", "title: Ada", "description: Ada's site.", "---", ""].join("\n"),
274
+ );
275
+ });
276
+ afterAll(() => {
277
+ rmSync(root, { recursive: true, force: true });
278
+ });
279
+
280
+ it("derives person mode from the kind: Identity (person) ✓", async () => {
281
+ const input = await buildSinglePageAuditInput({ slug: "home" }, root);
282
+ expect(input.identityMode).toBe("person");
283
+ const report = runAudit(input, [entityMarkup]);
284
+ expect(evidenceOf(report, "entity-markup")?.evidence).toContain("Identity (person) ✓");
285
+ });
286
+ });
287
+ });
@@ -10,18 +10,18 @@
10
10
  * error, never logged. This is the ordinary "site has nothing of this
11
11
  * type" case every real adapter (MDX, database) takes today.
12
12
  * - a type whose adapter call THROWS is a genuine failure. It must not take
13
- * down the audit (the other four reads still complete and their pages
13
+ * down the audit (the other two reads still complete and their pages
14
14
  * still count), but it must be visible — logged via `console.error`
15
15
  * (stderr, NOT stdout — the default stdio MCP transport's JSON-RPC
16
16
  * framing channel, #6012 round 2) — rather than silently collapsing into
17
17
  * the same vacuous "nothing found" result.
18
18
  *
19
- * The call-count assertions below prove that all five reads still ran despite
19
+ * The call-count assertions below prove that all three reads still ran despite
20
20
  * the throw — the read AFTER the failing one is not skipped, which a
21
21
  * sequential loop that ABORTED on the first throw would fail to show. They
22
22
  * are NOT, on their own, proof of CONCURRENCY: the prior sequential `for`
23
23
  * loop this replaced used `continue` (not a re-throw), so it too issued all
24
- * five calls. The `Promise.all` in the source is what delivers the
24
+ * three calls. The `Promise.all` in the source is what delivers the
25
25
  * concurrency AC; these assertions cover the narrower, still load-bearing
26
26
  * property that a throw part-way through never truncates the scan.
27
27
  */
@@ -93,10 +93,10 @@ describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () =>
93
93
 
94
94
  expect(result.readiness!.sitePages).toEqual([]);
95
95
  expect(errorSpy).not.toHaveBeenCalled();
96
- // All five reads still ran — two listDetail (Product, Service), three
97
- // listPages (the three policy types).
96
+ // All three reads still ran — two listDetail (Product, Service), one
97
+ // listPages (PurchasePolicyPage, #6646).
98
98
  expect(listDetail).toHaveBeenCalledTimes(2);
99
- expect(listPages).toHaveBeenCalledTimes(3);
99
+ expect(listPages).toHaveBeenCalledTimes(1);
100
100
  });
101
101
 
102
102
  it("a type whose adapter call throws is logged to stderr and does not fail the audit", async () => {
@@ -135,9 +135,57 @@ describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () =>
135
135
  // the stdio transport's JSON-RPC channel).
136
136
  expect(errorSpy).toHaveBeenCalledTimes(1);
137
137
  expect(errorSpy.mock.calls[0]![0]).toMatch(/Service/);
138
- // All five reads still ran despite the throw — the read AFTER the
139
- // throwing one (ReturnsPolicyPage, et al.) was not skipped.
138
+ // All three reads still ran despite the throw — the read AFTER the
139
+ // throwing one (PurchasePolicyPage) was not skipped.
140
140
  expect(listDetail).toHaveBeenCalledTimes(2);
141
- expect(listPages).toHaveBeenCalledTimes(3);
141
+ expect(listPages).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ it("a PurchasePolicyPage contributes its document-chassis section ids (#6646)", async () => {
145
+ const purchasePolicy: ContentPage = {
146
+ type: "PurchasePolicyPage",
147
+ slug: "purchase-policy",
148
+ title: "Purchase Policy",
149
+ description: "Returns, shipping and refunds.",
150
+ sections: [
151
+ {
152
+ id: "purchase-policy-document",
153
+ type: "document-chassis",
154
+ data: {
155
+ headline: "Purchase Policy",
156
+ sections: [
157
+ { id: "returns", heading: "Returns", blocks: [] },
158
+ { id: "shipping", heading: "Shipping", blocks: [] },
159
+ { id: "refunds", heading: "Refunds", blocks: [] },
160
+ ],
161
+ },
162
+ },
163
+ ] as ContentPage["sections"],
164
+ };
165
+ mockResolveContentAdapter.mockReturnValue({
166
+ getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
167
+ listPages: jest.fn().mockResolvedValue([purchasePolicy]),
168
+ listDetail: jest.fn().mockResolvedValue([]),
169
+ getDetailBySlug: jest.fn(),
170
+ searchPages: jest.fn(),
171
+ extractStructuredData: jest.fn(),
172
+ createPage: jest.fn(),
173
+ updatePage: jest.fn(),
174
+ deletePage: jest.fn(),
175
+ getPageVersion: jest.fn(),
176
+ });
177
+
178
+ const result = await buildSinglePageAuditInput({
179
+ slug: "home",
180
+ type: "HomePage",
181
+ });
182
+
183
+ expect(result.readiness!.sitePages).toEqual([
184
+ {
185
+ slug: "purchase-policy",
186
+ type: "PurchasePolicyPage",
187
+ chassisSectionIds: ["returns", "shipping", "refunds"],
188
+ },
189
+ ]);
142
190
  });
143
191
  });
@@ -103,7 +103,6 @@ const MCP_ONLY_TOOLS = new Set([
103
103
  "ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
104
104
  "ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
105
105
  "ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
106
- "ailkSubscribeNewsletter", // subscribe_newsletter — anonymous write via MCP; no Server Action (B7)
107
106
  "ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
108
107
  "ailkCreateProject", // create_project — agent-only project creator; no Server Action equivalent (#299)
109
108
  "ailkGetFlow", // get_flow — agent-callable flow-engine read; no web Server Action (#3956 D7)
@@ -228,6 +227,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
228
227
  // ailkSubmitFlowStep): twenty tools + three paired Server Actions.
229
228
  // - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
230
229
  // - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
230
+ // - ailkSubscribeNewsletter: paired with subscribeNewsletterAction
231
+ // (apps/web/app/[locale]/newsletter/newsletter.actions.ts, #6730 D9).
231
232
  // - ailkSubmitFlowStep: paired with submitFlowStepAction
232
233
  // (apps/web/app/[locale]/flows/[slug]/flow.actions.ts, #3957).
233
234
  // - startCheckoutAction, saveRecommendationAction: WEB-ONLY (#4842 D7,
@@ -241,8 +242,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
241
242
  // ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
242
243
  // MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
243
244
  // - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
244
- // - ailkSubscribeNewsletter, ailkRequestCallback:
245
- // MCP-only anonymous-write tools (B7; in MCP_ONLY_TOOLS allow-list).
245
+ // - ailkRequestCallback: MCP-only anonymous-write tool (B7; in
246
+ // MCP_ONLY_TOOLS allow-list).
246
247
  // - ailkCreateProject: MCP-only agent project creator (#299; in MCP_ONLY_TOOLS).
247
248
  // - ailkCreatePage, ailkUpdatePage, ailkDeletePage: RETIRED by #5853
248
249
  // (D7). Their HTTP twins — the untenanted /v1/content writes — were
@@ -264,7 +265,7 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
264
265
  // - ailkGetEventMeta: MCP-only meta-only scheduling read, twin of
265
266
  // GET /v1/schedule/event-meta (#4633) — same posture as the other
266
267
  // scheduling tools.
267
- expect(actions.size).toBe(5);
268
+ expect(actions.size).toBe(6);
268
269
  expect(tools.size).toBe(20);
269
270
  expect(r.missingTools).toEqual([]);
270
271
  expect(r.orphanTools).toEqual([]);
@@ -9,7 +9,8 @@
9
9
  "lint": "eslint .",
10
10
  "build": "tsc -b",
11
11
  "type-check": "tsc --noEmit",
12
- "dev": "node --experimental-vm-modules dist/index.js",
12
+ "dev": "tsx watch src/index.ts",
13
+ "start": "node --experimental-vm-modules dist/index.js",
13
14
  "test": "jest",
14
15
  "test:watch": "jest --watch",
15
16
  "test:coverage": "jest --coverage"
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { initMcpCommerceClaim } from "./lib/commerce-claim.js";
25
+ import { registerLicenseKeyStore } from "./lib/license-key-store.js";
25
26
  import { initMcpPublicWorkspace } from "./lib/public-workspace.js";
26
27
 
27
28
  // Resolves the MCP server's own workspace ONCE, before either transport
@@ -48,6 +49,12 @@ try {
48
49
  );
49
50
  }
50
51
 
52
+ // Registers the renewed licence key's durable home (#6677 DD-6) BEFORE the
53
+ // commerce claim below memoises the licence: with a database, the key a
54
+ // sibling app renewed lives in one SiteSetting row, and the claim must read
55
+ // it. Never throws; without DATABASE_URL the file store stays in place.
56
+ await registerLicenseKeyStore();
57
+
51
58
  // Resolves the deployment's `commerce` add-on claim ONCE, before either
52
59
  // transport branch — the sibling of the workspace resolution above and the
53
60
  // same #5876 idiom (#5971 spec D2). Without this await the memoised claim is