create-ailk 0.5.0 → 0.6.1

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 (329) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/cli.js +54 -5
  5. package/dist/copy.d.ts +14 -0
  6. package/dist/copy.js +38 -1
  7. package/dist/lib/api-boot-assertion.d.ts +46 -0
  8. package/dist/lib/api-boot-assertion.js +70 -0
  9. package/dist/lib/extract-module-bundle.d.ts +20 -1
  10. package/dist/lib/extract-module-bundle.js +64 -5
  11. package/dist/lib/import-graph.d.ts +29 -0
  12. package/dist/lib/import-graph.js +140 -0
  13. package/dist/lib/module-license-gate.d.ts +6 -1
  14. package/dist/lib/module-license-gate.js +6 -1
  15. package/dist/lib/pro-callout.js +1 -1
  16. package/dist/module-architecture.d.ts +1 -0
  17. package/dist/module-architecture.js +253 -127
  18. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  19. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  20. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  21. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  22. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  23. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  24. package/dist/programmatic.js +14 -10
  25. package/dist/surfaces.js +105 -7
  26. package/dist/template-manifest.d.ts +33 -0
  27. package/dist/template-manifest.js +99 -16
  28. package/dist/template-names.d.ts +1 -1
  29. package/dist/template-names.js +1 -0
  30. package/package.json +3 -2
  31. package/site-templates/marketing-site/DESIGN.md +148 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  34. package/site-templates/marketing-site/ailk-template.json +1 -1
  35. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  38. package/site-templates/minimalism-site/DESIGN.md +148 -0
  39. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  41. package/site-templates/minimalism-site/ailk-template.json +2 -1
  42. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  43. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  44. package/site-templates/saas-product/DESIGN.md +148 -0
  45. package/site-templates/simple-service/DESIGN.md +148 -0
  46. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  47. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  48. package/site-templates/simple-service/ailk-template.json +10 -0
  49. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  50. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  51. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  52. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  53. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  54. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  55. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  56. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  57. package/site-templates/simple-service/provenance.json +4 -0
  58. package/templates/.env.example +72 -7
  59. package/templates/CLAUDE.md +2 -3
  60. package/templates/DESIGN.md +155 -0
  61. package/templates/README.md +42 -8
  62. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  63. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  64. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  65. package/templates/apps/api/.env.example +13 -0
  66. package/templates/apps/api/CLAUDE.md +26 -2
  67. package/templates/apps/api/jest.config.cjs +3 -0
  68. package/templates/apps/api/package.json +1 -0
  69. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  70. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  71. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  72. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  73. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  74. package/templates/apps/api/src/config/index.ts +62 -0
  75. package/templates/apps/api/src/config/modules.ts +7 -3
  76. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  77. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  78. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  79. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  81. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  82. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  83. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  84. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  85. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  86. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  87. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  88. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  89. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  90. package/templates/apps/api/src/lib/site.ts +208 -9
  91. package/templates/apps/api/src/lib/stripe.ts +66 -0
  92. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  93. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  94. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  95. package/templates/apps/api/src/openapi/spec.ts +543 -2
  96. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  97. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  98. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  99. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  100. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  101. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  102. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  103. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  104. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  105. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  106. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  107. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  108. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  109. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  110. package/templates/apps/api/src/routes/content/index.ts +13 -73
  111. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  112. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  113. package/templates/apps/api/src/routes/content/search.ts +129 -0
  114. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  115. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  116. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  120. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  121. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  122. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  123. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  124. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  125. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  126. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  127. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  128. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  129. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  130. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  131. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  132. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  133. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  134. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  135. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  136. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  137. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  138. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  139. package/templates/apps/api/src/server.ts +439 -49
  140. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  141. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  142. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  143. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  144. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  145. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  146. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  147. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  148. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  149. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  150. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  151. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  152. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  153. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  154. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  155. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  156. package/templates/apps/api/src/services/audit-log.ts +668 -0
  157. package/templates/apps/api/src/services/consent.ts +18 -0
  158. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  159. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  160. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  161. package/templates/apps/api/src/services/entitlements.ts +80 -0
  162. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  163. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  164. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  165. package/templates/apps/api/src/services/listing-config.ts +27 -9
  166. package/templates/apps/api/src/services/project-listings.ts +201 -6
  167. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  168. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  169. package/templates/apps/api/src/services/revalidation.ts +173 -0
  170. package/templates/apps/api/src/services/verified-email.ts +71 -0
  171. package/templates/apps/api/src/vercel-handler.ts +75 -35
  172. package/templates/apps/mcp/.env.example +17 -0
  173. package/templates/apps/mcp/CLAUDE.md +57 -24
  174. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  175. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  176. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  177. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  178. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  179. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  180. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  181. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  182. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  183. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  184. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  185. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  186. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  187. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  188. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  189. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  190. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  191. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  192. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  193. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  194. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  195. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  196. package/templates/apps/mcp/jest.config.cjs +13 -0
  197. package/templates/apps/mcp/package.json +2 -0
  198. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  199. package/templates/apps/mcp/src/config/modules.ts +10 -5
  200. package/templates/apps/mcp/src/http-transport.ts +102 -29
  201. package/templates/apps/mcp/src/index.ts +73 -11
  202. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  203. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  204. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  205. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  206. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  207. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  208. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  209. package/templates/apps/mcp/src/server.ts +59 -35
  210. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  211. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  212. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  213. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  214. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  215. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  216. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  217. package/templates/apps/mcp/src/tools/index.ts +6 -4
  218. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  219. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  220. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  221. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  222. package/templates/apps/web/.env.example +71 -5
  223. package/templates/apps/web/CLAUDE.md +44 -0
  224. package/templates/apps/web/PRODUCT.md +86 -0
  225. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  226. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  227. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  228. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  229. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  230. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  231. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  232. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  233. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  234. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  235. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  236. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  237. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  238. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  239. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  240. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  241. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  242. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  243. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  244. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  245. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  246. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  247. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  248. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  249. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  250. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  251. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  252. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  253. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  254. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  255. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  256. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  257. package/templates/apps/web/auth.ts +407 -8
  258. package/templates/apps/web/jest.config.cjs +8 -4
  259. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  260. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  261. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  262. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  263. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  264. package/templates/apps/web/lib/auth-email.ts +318 -0
  265. package/templates/apps/web/lib/auth-env.ts +85 -0
  266. package/templates/apps/web/lib/auth-routes.ts +55 -0
  267. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  268. package/templates/apps/web/lib/site-brand.tsx +4 -8
  269. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  270. package/templates/apps/web/messages/en.json +187 -0
  271. package/templates/apps/web/middleware.ts +1 -1
  272. package/templates/apps/web/package.json +2 -1
  273. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  274. package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
  275. package/templates/content/_site.mdx +18 -3
  276. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  277. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  278. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  279. package/templates/database/CHANGELOG.md +149 -0
  280. package/templates/database/README.md +10 -3
  281. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  282. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  283. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  284. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  285. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  286. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  287. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  288. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  289. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  290. package/templates/database/__tests__/tenancy.test.ts +141 -0
  291. package/templates/database/config/schema.prisma +79 -2
  292. package/templates/database/content/schema.prisma +87 -11
  293. package/templates/database/inbox/schema.prisma +452 -9
  294. package/templates/database/jest.config.cjs +5 -3
  295. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  296. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  297. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  298. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  299. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  300. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  301. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  302. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  303. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  304. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  305. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  306. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  307. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  308. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  309. package/templates/database/package.json +3 -2
  310. package/templates/database/tenancy.ts +86 -1
  311. package/templates/database/tsconfig.json +1 -0
  312. package/versions.json +34 -34
  313. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  314. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  315. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  316. package/templates/apps/api/src/routes/content/create.ts +0 -115
  317. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  318. package/templates/apps/api/src/routes/content/update.ts +0 -112
  319. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  320. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  321. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  322. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  323. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  324. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  325. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  326. package/templates/apps/web/public/logo-dark.svg +0 -21
  327. package/templates/apps/web/public/logo-light.svg +0 -21
  328. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  329. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * @file artifact-content-store.db.test.ts
3
+ *
4
+ * LIVE-DATABASE proof of roadmap D1/D7 (#5800) — the generalized `Artifact` and
5
+ * the workspace-scoped `Asset`. Requires DATABASE_URL pointing at a Postgres
6
+ * with the migrations applied (`prisma migrate deploy`); the whole suite SKIPS
7
+ * when DATABASE_URL is unset, matching `prefixed-ids.db.test.ts` and keeping the
8
+ * workspace Jest run DB-free.
9
+ *
10
+ * Raw SQL through `pg` for the same reason that file gives: the generated Prisma
11
+ * client is ESM-only and cannot load under this package's CJS ts-jest transform,
12
+ * and raw SQL is the sharper proof anyway — a constraint must hold on the path
13
+ * no application helper can guard.
14
+ *
15
+ * What it proves:
16
+ * 1. THE SURVIVAL PROPERTY (D1). An artifact produced inside an agent session
17
+ * outlives that session: deleting the session sets `session_id` to NULL and
18
+ * leaves the row, its versions, and its publish pointer intact. Before this
19
+ * migration the FK was `ON DELETE CASCADE` on a NOT NULL column, so the
20
+ * same delete destroyed the workspace's content. This is the whole point of
21
+ * "the session becomes provenance, the workspace owns the artifact".
22
+ * 2. The identity of a published page — `(workspace_id, content_type, locale,
23
+ * slug)` is unique, while unpublished drafts (slug NULL) are unconstrained,
24
+ * because NULLs never collide in a Postgres unique index.
25
+ * 3. `published_version_id` is an independent pointer: it is NOT `is_current`,
26
+ * and appending a newer draft tip does not move it.
27
+ * 4. `assets` is workspace-scoped with a unique `(workspace_id, key)` and a
28
+ * DB-minted `asset_` UUIDv7 id (ADR-0031).
29
+ */
30
+ import { randomUUID } from "node:crypto";
31
+
32
+ import { Client } from "pg";
33
+
34
+ import { DEFAULT_WORKSPACE_ID } from "../tenancy.js";
35
+
36
+ const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
37
+
38
+ const V7_RE =
39
+ /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
40
+
41
+ /** A run-unique suffix so repeated local runs never collide on the unique key. */
42
+ const RUN = randomUUID().slice(0, 8);
43
+
44
+ describeDb("Artifact content store (roadmap D1/D7, live DB)", () => {
45
+ let db: Client;
46
+
47
+ beforeAll(async () => {
48
+ db = new Client({ connectionString: process.env.DATABASE_URL });
49
+ await db.connect();
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await db.query(
54
+ `DELETE FROM "artifacts" WHERE "title" LIKE $1`,
55
+ [`5800-${RUN}%`],
56
+ );
57
+ await db.query(`DELETE FROM "agent_sessions" WHERE "title" LIKE $1`, [
58
+ `5800-${RUN}%`,
59
+ ]);
60
+ await db.query(`DELETE FROM "assets" WHERE "key" LIKE $1`, [
61
+ `5800-${RUN}%`,
62
+ ]);
63
+ await db.end();
64
+ });
65
+
66
+ describe("the session is provenance — deleting it never destroys content", () => {
67
+ it("sets session_id NULL and leaves the artifact, its versions and its publish pointer", async () => {
68
+ const sessionId = (
69
+ await db.query<{ id: string }>(
70
+ `INSERT INTO "agent_sessions" ("workspace_id", "title", "updated_at")
71
+ VALUES ($1, $2, now()) RETURNING "id"`,
72
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} session`],
73
+ )
74
+ ).rows[0]!.id;
75
+
76
+ const artifactId = (
77
+ await db.query<{ id: string }>(
78
+ `INSERT INTO "artifacts" ("session_id", "workspace_id", "title", "content_type", "locale", "slug", "updated_at")
79
+ VALUES ($1, $2, $3, 'text/page', 'en', $4, now()) RETURNING "id"`,
80
+ [
81
+ sessionId,
82
+ DEFAULT_WORKSPACE_ID,
83
+ `5800-${RUN} page`,
84
+ `5800-${RUN}-survivor`,
85
+ ],
86
+ )
87
+ ).rows[0]!.id;
88
+
89
+ const versionId = (
90
+ await db.query<{ id: string }>(
91
+ `INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
92
+ VALUES ($1, $2, '{"body":"v1"}'::jsonb, TRUE) RETURNING "id"`,
93
+ [artifactId, DEFAULT_WORKSPACE_ID],
94
+ )
95
+ ).rows[0]!.id;
96
+
97
+ await db.query(
98
+ `UPDATE "artifacts" SET "published_version_id" = $1 WHERE "id" = $2`,
99
+ [versionId, artifactId],
100
+ );
101
+
102
+ // The act under test: the producing session is deleted outright.
103
+ await db.query(`DELETE FROM "agent_sessions" WHERE "id" = $1`, [
104
+ sessionId,
105
+ ]);
106
+
107
+ const { rows } = await db.query<{
108
+ session_id: string | null;
109
+ workspace_id: string;
110
+ published_version_id: string | null;
111
+ }>(
112
+ `SELECT "session_id", "workspace_id", "published_version_id"
113
+ FROM "artifacts" WHERE "id" = $1`,
114
+ [artifactId],
115
+ );
116
+
117
+ expect(rows).toHaveLength(1);
118
+ expect(rows[0]!.session_id).toBeNull();
119
+ expect(rows[0]!.workspace_id).toBe(DEFAULT_WORKSPACE_ID);
120
+ expect(rows[0]!.published_version_id).toBe(versionId);
121
+
122
+ const versions = await db.query(
123
+ `SELECT "id" FROM "artifact_versions" WHERE "artifact_id" = $1`,
124
+ [artifactId],
125
+ );
126
+ expect(versions.rows).toHaveLength(1);
127
+ });
128
+
129
+ it("accepts an artifact with no session at all — the workspace owns it", async () => {
130
+ const { rows } = await db.query<{ id: string; session_id: string | null }>(
131
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
132
+ VALUES ($1, $2, 'text/page', 'en', now()) RETURNING "id", "session_id"`,
133
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} sessionless`],
134
+ );
135
+
136
+ expect(rows[0]!.session_id).toBeNull();
137
+ expect(rows[0]!.id).toMatch(/^artif_/);
138
+ });
139
+ });
140
+
141
+ describe("published identity", () => {
142
+ it("rejects a second artifact with the same (workspace, contentType, locale, slug)", async () => {
143
+ const slug = `5800-${RUN}-dupe`;
144
+ await db.query(
145
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
146
+ VALUES ($1, $2, 'text/page', 'en', $3, now())`,
147
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} first`, slug],
148
+ );
149
+
150
+ await expect(
151
+ db.query(
152
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
153
+ VALUES ($1, $2, 'text/page', 'en', $3, now())`,
154
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} second`, slug],
155
+ ),
156
+ ).rejects.toThrow(/duplicate key|unique/i);
157
+ });
158
+
159
+ it("admits many unpublished drafts — a NULL slug never collides", async () => {
160
+ for (const n of [1, 2, 3]) {
161
+ await db.query(
162
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
163
+ VALUES ($1, $2, 'text/page', 'en', now())`,
164
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} draft ${n}`],
165
+ );
166
+ }
167
+
168
+ const { rows } = await db.query<{ count: string }>(
169
+ `SELECT count(*)::text AS count FROM "artifacts"
170
+ WHERE "title" LIKE $1 AND "slug" IS NULL`,
171
+ [`5800-${RUN} draft %`],
172
+ );
173
+ expect(rows[0]!.count).toBe("3");
174
+ });
175
+
176
+ it("separates the same slug across locales", async () => {
177
+ const slug = `5800-${RUN}-locale`;
178
+ for (const locale of ["en", "fr"]) {
179
+ await db.query(
180
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
181
+ VALUES ($1, $2, 'text/page', $3, $4, now())`,
182
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} ${locale}`, locale, slug],
183
+ );
184
+ }
185
+
186
+ const { rows } = await db.query<{ count: string }>(
187
+ `SELECT count(*)::text AS count FROM "artifacts" WHERE "slug" = $1`,
188
+ [slug],
189
+ );
190
+ expect(rows[0]!.count).toBe("2");
191
+ });
192
+ });
193
+
194
+ describe("the publish pointer is not the draft tip", () => {
195
+ it("survives a newer is_current version", async () => {
196
+ const artifactId = (
197
+ await db.query<{ id: string }>(
198
+ `INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
199
+ VALUES ($1, $2, 'text/page', 'en', now()) RETURNING "id"`,
200
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN} pointer`],
201
+ )
202
+ ).rows[0]!.id;
203
+
204
+ const published = (
205
+ await db.query<{ id: string }>(
206
+ `INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
207
+ VALUES ($1, $2, '{"body":"published"}'::jsonb, TRUE) RETURNING "id"`,
208
+ [artifactId, DEFAULT_WORKSPACE_ID],
209
+ )
210
+ ).rows[0]!.id;
211
+
212
+ await db.query(
213
+ `UPDATE "artifacts" SET "published_version_id" = $1 WHERE "id" = $2`,
214
+ [published, artifactId],
215
+ );
216
+
217
+ // A newer draft tip lands; the publish pointer must NOT follow it.
218
+ await db.query(
219
+ `UPDATE "artifact_versions" SET "is_current" = FALSE WHERE "artifact_id" = $1`,
220
+ [artifactId],
221
+ );
222
+ const draft = (
223
+ await db.query<{ id: string }>(
224
+ `INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
225
+ VALUES ($1, $2, '{"body":"draft"}'::jsonb, TRUE) RETURNING "id"`,
226
+ [artifactId, DEFAULT_WORKSPACE_ID],
227
+ )
228
+ ).rows[0]!.id;
229
+
230
+ const { rows } = await db.query<{ published_version_id: string | null }>(
231
+ `SELECT "published_version_id" FROM "artifacts" WHERE "id" = $1`,
232
+ [artifactId],
233
+ );
234
+ expect(rows[0]!.published_version_id).toBe(published);
235
+ expect(rows[0]!.published_version_id).not.toBe(draft);
236
+ });
237
+ });
238
+
239
+ describe("Asset (D7)", () => {
240
+ it("mints an asset_ UUIDv7 id and stores the bytes", async () => {
241
+ const { rows } = await db.query<{ id: string; size: number }>(
242
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
243
+ VALUES ($1, $2, 'logo.png', 'image/png', $3, 4) RETURNING "id", "size"`,
244
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN}-logo`, Buffer.from([1, 2, 3, 4])],
245
+ );
246
+
247
+ expect(rows[0]!.id).toMatch(/^asset_/);
248
+ expect(rows[0]!.id.slice("asset_".length)).toMatch(V7_RE);
249
+ expect(rows[0]!.size).toBe(4);
250
+ });
251
+
252
+ it("rejects a second asset with the same (workspace, key)", async () => {
253
+ const key = `5800-${RUN}-dupe-key`;
254
+ await db.query(
255
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
256
+ VALUES ($1, $2, 'a.png', 'image/png', $3, 1)`,
257
+ [DEFAULT_WORKSPACE_ID, key, Buffer.from([1])],
258
+ );
259
+
260
+ await expect(
261
+ db.query(
262
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
263
+ VALUES ($1, $2, 'b.png', 'image/png', $3, 1)`,
264
+ [DEFAULT_WORKSPACE_ID, key, Buffer.from([2])],
265
+ ),
266
+ ).rejects.toThrow(/duplicate key|unique/i);
267
+ });
268
+
269
+ it("rejects a malformed workspace_id (ADR-0031 CHECK)", async () => {
270
+ await expect(
271
+ db.query(
272
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
273
+ VALUES ('not-a-workspace-id', $1, 'a.png', 'image/png', $2, 1)`,
274
+ [`5800-${RUN}-bad-scope`, Buffer.from([1])],
275
+ ),
276
+ ).rejects.toThrow(/assets_workspace_id_format|check constraint/i);
277
+ });
278
+
279
+ // #6092 (S4/D10) — title, description and tags. `tags` stores SLUGS, never
280
+ // the human labels the lightbox renders (D3): 'ad-creative' and
281
+ // 'user-uploaded' are the exact pair the demo's first asset carries
282
+ // (demo.html:357).
283
+ it("stores and returns two tags", async () => {
284
+ const { rows } = await db.query<{ tags: string[] }>(
285
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size", "tags")
286
+ VALUES ($1, $2, 'tagged.png', 'image/png', $3, 1, $4) RETURNING "tags"`,
287
+ [
288
+ DEFAULT_WORKSPACE_ID,
289
+ `5800-${RUN}-tagged`,
290
+ Buffer.from([1]),
291
+ ["ad-creative", "user-uploaded"],
292
+ ],
293
+ );
294
+
295
+ expect(rows[0]!.tags).toEqual(["ad-creative", "user-uploaded"]);
296
+ expect(rows[0]!.tags).toHaveLength(2);
297
+ });
298
+
299
+ it("defaults title, description and tags when none of the three columns is named", async () => {
300
+ const { rows } = await db.query<{
301
+ title: string | null;
302
+ description: string | null;
303
+ tags: string[];
304
+ }>(
305
+ `INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
306
+ VALUES ($1, $2, 'untagged.png', 'image/png', $3, 1)
307
+ RETURNING "title", "description", "tags"`,
308
+ [DEFAULT_WORKSPACE_ID, `5800-${RUN}-untagged`, Buffer.from([1])],
309
+ );
310
+
311
+ expect(rows[0]!.title).toBeNull();
312
+ expect(rows[0]!.description).toBeNull();
313
+ expect(rows[0]!.tags).toEqual([]);
314
+ });
315
+ });
316
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @file composite-fence.db.test.ts
3
+ *
4
+ * LIVE-DATABASE proof of the #6254 keystone constraints (AC-2, AC-5, AC-7).
5
+ * Requires DATABASE_URL pointing at a Postgres with the migrations applied
6
+ * (`prisma migrate deploy`); the whole suite SKIPS when DATABASE_URL is
7
+ * unset, matching `flow-workspace-key.db.test.ts` / `prefixed-ids.db.test.ts`
8
+ * and keeping the workspace Jest run DB-free.
9
+ *
10
+ * Raw SQL through `pg`, for the same reason those files give: the generated
11
+ * Prisma client is ESM-only and cannot load under this package's CJS ts-jest
12
+ * transform.
13
+ *
14
+ * This suite proves the CONSTRAINTS database/migrations/
15
+ * 20260922120000_tenancy_fence adds — it does not re-prove that migration's
16
+ * own backfill (that is `flow-workspace-key.db.test.ts`'s job for its two
17
+ * models, and this migration's backfill is largely a defensive re-run of
18
+ * work the #6155/#6203 precedent migrations already did — see this
19
+ * migration's own header comment). What it proves (spec § 8.2):
20
+ *
21
+ * 1. `Lead` rejects a row whose `workspaceId` disagrees with its `siteId`
22
+ * — SQLSTATE 23514 (AC-5's core).
23
+ * 2. `Lead` accepts a row whose `workspaceId` AGREES with its `siteId` —
24
+ * without this, assertion 1 would also pass against a table that
25
+ * rejects everything.
26
+ * 3. `FlowDefinition` rejects the same disagreement — AC-5's "at least
27
+ * one retrofit model besides Lead".
28
+ * 4. NULL is unstorable on `Lead` and `FlowDefinition` — SQLSTATE 23502
29
+ * (not_null_violation) — AC-7.
30
+ * 5. `WorkspaceSiteIdentity` carries the AC-2 composite unique constraint,
31
+ * proven at the database, not just in the schema file.
32
+ * 6. The migration's own literal sentinel id equals `DEFAULT_WORKSPACE_ID`
33
+ * — the two cannot drift apart (the #6155/#6203 precedents' own
34
+ * technique).
35
+ */
36
+ import { randomUUID } from "node:crypto";
37
+ import { readFileSync } from "node:fs";
38
+ import { join } from "node:path";
39
+
40
+ import { Client } from "pg";
41
+
42
+ import { DEFAULT_WORKSPACE_ID } from "../tenancy.js";
43
+
44
+ const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
45
+
46
+ /** A run-unique suffix so repeated local runs never collide. */
47
+ const RUN = randomUUID().slice(0, 8);
48
+
49
+ const MIGRATION_SQL_PATH = join(
50
+ __dirname,
51
+ "..",
52
+ "migrations",
53
+ "20260922120000_tenancy_fence",
54
+ "migration.sql",
55
+ );
56
+ const MIGRATION_SQL = readFileSync(MIGRATION_SQL_PATH, "utf-8");
57
+
58
+ /** Postgres's own error shape carries `code` as a 5-character SQLSTATE. */
59
+ type PgError = { code?: string };
60
+
61
+ describeDb("the #6254 tenancy-fence constraints (live DB)", () => {
62
+ let db: Client;
63
+ let workspaceAId: string;
64
+ let workspaceBId: string;
65
+
66
+ beforeAll(async () => {
67
+ db = new Client({ connectionString: process.env.DATABASE_URL });
68
+ await db.connect();
69
+
70
+ // Two REAL, distinct workspaces — A and B — so a "disagreement" insert
71
+ // names two values that both actually exist, not a real one versus
72
+ // garbage (which the format CHECK, not the equality CHECK, would catch).
73
+ const org = (
74
+ await db.query<{ id: string }>(
75
+ `INSERT INTO "organizations" ("name", "slug", "updated_at")
76
+ VALUES ($1, $2, now()) RETURNING "id"`,
77
+ [`6254 Org ${RUN}`, `6254-org-${RUN}`],
78
+ )
79
+ ).rows[0]!;
80
+
81
+ workspaceAId = (
82
+ await db.query<{ id: string }>(
83
+ `INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
84
+ VALUES ($1, $2, $3, now()) RETURNING "id"`,
85
+ [org.id, `6254 Workspace A ${RUN}`, `6254-ws-a-${RUN}`],
86
+ )
87
+ ).rows[0]!.id;
88
+
89
+ workspaceBId = (
90
+ await db.query<{ id: string }>(
91
+ `INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
92
+ VALUES ($1, $2, $3, now()) RETURNING "id"`,
93
+ [org.id, `6254 Workspace B ${RUN}`, `6254-ws-b-${RUN}`],
94
+ )
95
+ ).rows[0]!.id;
96
+ });
97
+
98
+ afterAll(async () => {
99
+ await db.query(`DELETE FROM "flow_definitions" WHERE "slug" LIKE $1`, [
100
+ `6254-${RUN}-%`,
101
+ ]);
102
+ await db.query(`DELETE FROM "leads" WHERE "source" = $1`, [
103
+ `6254-test-${RUN}`,
104
+ ]);
105
+ await db.query(`DELETE FROM "workspaces" WHERE "slug" LIKE $1`, [
106
+ `6254-ws-%-${RUN}`,
107
+ ]);
108
+ await db.query(`DELETE FROM "organizations" WHERE "slug" = $1`, [
109
+ `6254-org-${RUN}`,
110
+ ]);
111
+ await db.end();
112
+ });
113
+
114
+ describe("AC-5 — the equality CHECK rejects a site<->workspace disagreement", () => {
115
+ it("Lead: workspace_id disagreeing with site_id raises 23514", async () => {
116
+ await expect(
117
+ db.query(
118
+ `INSERT INTO "leads" ("site_id", "workspace_id", "source", "email", "payload")
119
+ VALUES ($1, $2, $3, $4, '{}'::jsonb)`,
120
+ [workspaceAId, workspaceBId, `6254-test-${RUN}`, "disagree@example.com"],
121
+ ),
122
+ ).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23514" });
123
+ });
124
+
125
+ it("Lead: workspace_id AGREEING with site_id succeeds", async () => {
126
+ const result = await db.query<{ id: string }>(
127
+ `INSERT INTO "leads" ("site_id", "workspace_id", "source", "email", "payload")
128
+ VALUES ($1, $1, $2, $3, '{}'::jsonb) RETURNING "id"`,
129
+ [workspaceAId, `6254-test-${RUN}`, "agree@example.com"],
130
+ );
131
+ expect(result.rows).toHaveLength(1);
132
+ });
133
+
134
+ it("FlowDefinition (the second retrofit model, AC-5's own requirement): workspace_id disagreeing with site_id raises 23514", async () => {
135
+ await expect(
136
+ db.query(
137
+ `INSERT INTO "flow_definitions"
138
+ ("site_id", "workspace_id", "slug", "config", "title", "status", "updated_at")
139
+ VALUES ($1, $2, $3, '{}'::jsonb, 'Test', 'live', now())`,
140
+ [workspaceAId, workspaceBId, `6254-${RUN}-disagree`],
141
+ ),
142
+ ).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23514" });
143
+ });
144
+
145
+ it("FlowDefinition: workspace_id AGREEING with site_id succeeds", async () => {
146
+ const result = await db.query<{ id: string }>(
147
+ `INSERT INTO "flow_definitions"
148
+ ("site_id", "workspace_id", "slug", "config", "title", "status", "updated_at")
149
+ VALUES ($1, $1, $2, '{}'::jsonb, 'Test', 'live', now()) RETURNING "id"`,
150
+ [workspaceAId, `6254-${RUN}-agree`],
151
+ );
152
+ expect(result.rows).toHaveLength(1);
153
+ });
154
+ });
155
+
156
+ describe("AC-7 — NOT NULL: no row can exist with workspace_id IS NULL", () => {
157
+ it("Lead: omitting workspace_id raises 23502 (not_null_violation)", async () => {
158
+ await expect(
159
+ db.query(
160
+ `INSERT INTO "leads" ("site_id", "source", "email", "payload")
161
+ VALUES ($1, $2, $3, '{}'::jsonb)`,
162
+ [workspaceAId, `6254-test-${RUN}`, "null@example.com"],
163
+ ),
164
+ ).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23502" });
165
+ });
166
+
167
+ it("FlowDefinition: omitting workspace_id raises 23502 (not_null_violation)", async () => {
168
+ await expect(
169
+ db.query(
170
+ `INSERT INTO "flow_definitions" ("site_id", "slug", "config", "title", "status", "updated_at")
171
+ VALUES ($1, $2, '{}'::jsonb, 'Test', 'live', now())`,
172
+ [workspaceAId, `6254-${RUN}-null`],
173
+ ),
174
+ ).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23502" });
175
+ });
176
+ });
177
+
178
+ describe("AC-2 — WorkspaceSiteIdentity's composite unique exists at the database", () => {
179
+ it("pg_constraint reports a UNIQUE constraint over (id, workspace_id)", async () => {
180
+ const result = await db.query<{ contype: string; conkey_count: number }>(
181
+ `SELECT c.contype, array_length(c.conkey, 1) AS conkey_count
182
+ FROM pg_constraint c
183
+ JOIN pg_class t ON t.oid = c.conrelid
184
+ WHERE t.relname = 'workspace_site_identities'
185
+ AND c.conname = 'workspace_site_identities_id_workspace_id_key'`,
186
+ );
187
+ expect(result.rows).toHaveLength(1);
188
+ expect(result.rows[0]!.contype).toBe("u");
189
+ expect(result.rows[0]!.conkey_count).toBe(2);
190
+ });
191
+ });
192
+
193
+ describe("the migration's own literal sentinel id equals DEFAULT_WORKSPACE_ID", () => {
194
+ it("cannot drift apart — read from the shipped file, not a re-typed copy", () => {
195
+ // SQL cannot import database/tenancy.ts (spec § 5.3 step 5), so the
196
+ // migration carries the sentinel as a literal. Pin it against the
197
+ // exported constant by reading the shipped file, exactly as the
198
+ // #6155/#6203 precedents do.
199
+ const occurrences = MIGRATION_SQL.match(/'wksp_[0-9a-f-]+'/g) ?? [];
200
+ expect(occurrences.length).toBeGreaterThan(0);
201
+ for (const literal of occurrences) {
202
+ expect(literal).toBe(`'${DEFAULT_WORKSPACE_ID}'`);
203
+ }
204
+ });
205
+ });
206
+ });