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
@@ -10,10 +10,12 @@ module.exports = {
10
10
  // ts-jest compiles it directly (#3958) — mirrors apps/api/jest.config.cjs's
11
11
  // identical mapping. The published dist is ESM-only and this package's
12
12
  // CJS ts-jest transform can't load it; database/flows/presets.ts's test
13
- // needs the real flowConfigSchema value at runtime (a value import, not a
14
- // type-only one — unlike ../tenancy.ts's `import type { Id }` from
15
- // @working-theory/ids, which erases to nothing and needs no mapping).
13
+ // needs the real flowConfigSchema value at runtime.
16
14
  "^@working-theory/validation$": "<rootDir>/../packages/validation/src/index.ts",
15
+ // ../tenancy.ts's `isSentinel` import from @working-theory/ids became a
16
+ // VALUE import (#5876 spec D2/D11) — no longer type-only, so it DOES need
17
+ // a mapping now, same reason as the validation mapping above.
18
+ "^@working-theory/ids$": "<rootDir>/../packages/ids/src/index.ts",
17
19
  "^(\\.{1,2}/.*)\\.js$": "$1",
18
20
  },
19
21
  transform: {
@@ -0,0 +1,15 @@
1
+ -- The founder's "how are you solving it?" answer on `project_listings` (#5096).
2
+ --
3
+ -- `problem` (step five) has had a column since the surface shipped; its
4
+ -- narrative other half has not, so samhenry PR #324's step six had nowhere to
5
+ -- write.
6
+ --
7
+ -- ONE NULLABLE COLUMN, NO DEFAULT, NO BACKFILL, NO CHANGE TO ANY EXISTING ONE.
8
+ -- Additive and opt-in: the field is absent from the platform's default
9
+ -- `collectedFields` mask (#5096 DD1), so no existing site can have it written
10
+ -- until that site names it. Every row written before this migration keeps a
11
+ -- NULL here, which the row schema already allows for every application field
12
+ -- (#5036 D1 — a half-filled draft is a legitimate row).
13
+ --
14
+ -- Rollback: ALTER TABLE "project_listings" DROP COLUMN "solution";
15
+ ALTER TABLE "project_listings" ADD COLUMN "solution" TEXT;
@@ -0,0 +1,158 @@
1
+ -- #5800 — the Artifact content store: roadmap D1 (generalize `Artifact`) and
2
+ -- D7 (`Asset`, workspace-scoped).
3
+ --
4
+ -- D1's one sentence: THE SESSION BECOMES PROVENANCE, THE WORKSPACE OWNS THE
5
+ -- ARTIFACT. Today `artifacts.session_id` is NOT NULL behind an ON DELETE
6
+ -- CASCADE, so deleting a conversation deletes the pages it produced. After this
7
+ -- migration the column is nullable behind ON DELETE SET NULL: the session is
8
+ -- where the artifact came from, not what keeps it alive.
9
+ --
10
+ -- The relaxation is bracketed by ZERO-ROW ASSERTIONS. It applies to an empty
11
+ -- database and to a populated one, and each assertion names a specific way the
12
+ -- relaxation could have lost data. A failed assertion aborts the transaction
13
+ -- with a message that says which rows to look at — the migration never proceeds
14
+ -- on a silent surprise, and it never backfills its way past one.
15
+ --
16
+ -- The rest is additive: four nullable-or-defaulted columns, two indexes, and a
17
+ -- new `assets` table. `content_type` is BACKFILLED from the legacy `type`
18
+ -- column rather than replacing it, because
19
+ -- apps/api/src/routes/workspaces/sessions.ts (a hard-floor surface) reads
20
+ -- `type` and must keep working unchanged; the two agree row-for-row when this
21
+ -- migration commits, and retiring `type` belongs to the card that moves that
22
+ -- route.
23
+ --
24
+ -- Rollback: drop the two indexes and the four columns, drop "assets", restore
25
+ -- the FK as NOT NULL + ON DELETE CASCADE. That restore is only safe while no
26
+ -- artifact has yet outlived its session.
27
+
28
+ -- ── PRE-ASSERTIONS (zero rows, or this migration does not run) ──────────────
29
+ DO $$
30
+ DECLARE
31
+ already_null bigint;
32
+ orphaned bigint;
33
+ BEGIN
34
+ -- "session_id" is NOT NULL today. A non-zero count here means the column is
35
+ -- not the shape this migration was written against — stop rather than guess.
36
+ SELECT count(*) INTO already_null FROM "artifacts" WHERE "session_id" IS NULL;
37
+ IF already_null <> 0 THEN
38
+ RAISE EXCEPTION
39
+ '#5800 pre-assertion: % artifact row(s) already carry a NULL session_id before the FK was relaxed; the column was expected NOT NULL',
40
+ already_null;
41
+ END IF;
42
+
43
+ -- The outgoing FK is what guarantees this is zero. Proving it BEFORE the
44
+ -- swap is what lets the post-assertion below attribute any orphan to the
45
+ -- swap itself.
46
+ SELECT count(*) INTO orphaned
47
+ FROM "artifacts" a
48
+ LEFT JOIN "agent_sessions" s ON s."id" = a."session_id"
49
+ WHERE s."id" IS NULL;
50
+ IF orphaned <> 0 THEN
51
+ RAISE EXCEPTION
52
+ '#5800 pre-assertion: % artifact row(s) point at an agent_sessions row that does not exist',
53
+ orphaned;
54
+ END IF;
55
+ END $$;
56
+
57
+ -- The census the post-assertion compares against. ON COMMIT DROP: `prisma
58
+ -- migrate` runs each migration in one transaction, so this disappears with it.
59
+ CREATE TEMP TABLE "_5800_artifact_census" ON COMMIT DROP AS
60
+ SELECT count(*) AS n FROM "artifacts";
61
+
62
+ -- ── The FK relaxation (D1) ─────────────────────────────────────────────────
63
+ -- DropForeignKey
64
+ ALTER TABLE "artifacts" DROP CONSTRAINT "artifacts_session_id_fkey";
65
+
66
+ -- AlterTable
67
+ ALTER TABLE "artifacts" ADD COLUMN "content_type" TEXT NOT NULL DEFAULT 'text/document',
68
+ ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en',
69
+ ADD COLUMN "published_version_id" TEXT,
70
+ ADD COLUMN "slug" TEXT,
71
+ ALTER COLUMN "session_id" DROP NOT NULL;
72
+
73
+ -- AddForeignKey
74
+ ALTER TABLE "artifacts" ADD CONSTRAINT "artifacts_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "agent_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
75
+
76
+ -- Backfill: the new discriminator starts life agreeing with the legacy one, so
77
+ -- a pre-existing chat artifact reads identically through either column.
78
+ UPDATE "artifacts" SET "content_type" = "type";
79
+
80
+ -- ── POST-ASSERTIONS (zero rows lost, zero rows nulled, zero drift) ─────────
81
+ DO $$
82
+ DECLARE
83
+ before_n bigint;
84
+ after_n bigint;
85
+ nulled bigint;
86
+ drift bigint;
87
+ BEGIN
88
+ SELECT n INTO before_n FROM "_5800_artifact_census";
89
+ SELECT count(*) INTO after_n FROM "artifacts";
90
+ IF before_n <> after_n THEN
91
+ RAISE EXCEPTION
92
+ '#5800 post-assertion: artifact count changed across the FK relaxation (% -> %)',
93
+ before_n, after_n;
94
+ END IF;
95
+
96
+ -- Relaxing the constraint must not itself null anyone's provenance. Rows
97
+ -- become NULL later, one at a time, as sessions are deleted.
98
+ SELECT count(*) INTO nulled FROM "artifacts" WHERE "session_id" IS NULL;
99
+ IF nulled <> 0 THEN
100
+ RAISE EXCEPTION
101
+ '#5800 post-assertion: relaxing the FK left % artifact row(s) with a NULL session_id',
102
+ nulled;
103
+ END IF;
104
+
105
+ SELECT count(*) INTO drift
106
+ FROM "artifacts" WHERE "content_type" IS DISTINCT FROM "type";
107
+ IF drift <> 0 THEN
108
+ RAISE EXCEPTION
109
+ '#5800 post-assertion: % artifact row(s) disagree between content_type and the legacy type column',
110
+ drift;
111
+ END IF;
112
+ END $$;
113
+
114
+ -- ── Content-store identity + the list index (D1) ───────────────────────────
115
+ -- CreateIndex
116
+ CREATE INDEX "artifacts_workspace_id_content_type_updated_at_idx" ON "artifacts"("workspace_id", "content_type", "updated_at");
117
+
118
+ -- CreateIndex
119
+ -- `slug` is NULL until publish and NULLs never collide in a Postgres unique
120
+ -- index, so a workspace may hold any number of unpublished drafts while
121
+ -- exactly one artifact may claim a given published path.
122
+ CREATE UNIQUE INDEX "artifacts_workspace_id_content_type_locale_slug_key" ON "artifacts"("workspace_id", "content_type", "locale", "slug");
123
+
124
+ -- ── Asset (D7) ─────────────────────────────────────────────────────────────
125
+ -- Shaped after `deliverable_assets` (20260811050000_add_deliverable_models):
126
+ -- the bytes live in the row. Same CreateTable + CreateIndex + ADR-0031 rule-3
127
+ -- id-format completion in ONE migration as
128
+ -- 20260911180000_consent_grants, since the table is new and needs no retrofit.
129
+ -- CreateTable
130
+ CREATE TABLE "assets" (
131
+ "id" TEXT NOT NULL DEFAULT ('asset_'::text || (uuidv7())::text),
132
+ "workspace_id" TEXT NOT NULL,
133
+ "key" TEXT NOT NULL,
134
+ "filename" TEXT NOT NULL,
135
+ "content_type" TEXT NOT NULL,
136
+ "bytes" BYTEA NOT NULL,
137
+ "size" INTEGER NOT NULL,
138
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
139
+
140
+ CONSTRAINT "assets_pkey" PRIMARY KEY ("id")
141
+ );
142
+
143
+ -- CreateIndex
144
+ CREATE INDEX "assets_workspace_id_idx" ON "assets"("workspace_id");
145
+
146
+ -- CreateIndex
147
+ CREATE UNIQUE INDEX "assets_workspace_id_key_key" ON "assets"("workspace_id", "key");
148
+
149
+ -- ADR-0031 rule 3 — assets: asset_ (registered in
150
+ -- packages/ids/src/registry.ts ENTITY_PREFIX.asset; this migration's path is
151
+ -- added to migration-sync.test.ts's PK_CHECK_MIGRATIONS). workspace_id is a
152
+ -- relation-free scope column (ADR-0009 invariant 1) and gets the same CHECK
153
+ -- every other content-namespace table's does.
154
+ ALTER TABLE "assets" ALTER COLUMN "id" TYPE text COLLATE "C";
155
+ ALTER TABLE "assets" ALTER COLUMN "id" SET DEFAULT ('asset_'::text || (uuidv7())::text);
156
+ ALTER TABLE "assets" ADD CONSTRAINT "assets_id_format" CHECK ("id" ~ '^asset_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
157
+ ALTER TABLE "assets" ALTER COLUMN "workspace_id" TYPE text COLLATE "C";
158
+ ALTER TABLE "assets" ADD CONSTRAINT "assets_workspace_id_format" CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
@@ -0,0 +1,40 @@
1
+ -- #5970 — request-and-confirm: the callback row gains a confirm/pay lifecycle
2
+ -- (D-67 / SUB-100, spec docs/specs/5970-request-and-confirm-spec.md § 4.3).
3
+ --
4
+ -- Eleven nullable columns and two indexes on "callbacks". Every column is
5
+ -- NULL-able with no default, so on Postgres this ALTER TABLE is metadata-only:
6
+ -- no table rewrite, no backfill, no lock held for the length of a scan.
7
+ --
8
+ -- Nothing here relaxes a constraint and nothing here backfills, so — unlike
9
+ -- 20260916120000_artifact_content_store — this migration carries no zero-row
10
+ -- pre-assertion block: there is no way for it to lose a row.
11
+ --
12
+ -- The two indexes are the webhook stamp's `where` (spec § D8). Without them
13
+ -- every checkout.session.completed carrying a payment_link would sequential-
14
+ -- scan "callbacks" on a hot path. "stripe_checkout_session_id" carries no
15
+ -- index deliberately: nothing queries by it — it is a support-lookup column.
16
+ --
17
+ -- Rollback: drop the two indexes and the eleven columns. Safe unconditionally
18
+ -- while nothing but #5970's own code reads them.
19
+
20
+ ALTER TABLE "callbacks"
21
+ ADD COLUMN "confirm_token_hash" TEXT,
22
+ ADD COLUMN "confirm_token_expires_at" TIMESTAMP(3),
23
+ ADD COLUMN "confirmed_at" TIMESTAMP(3),
24
+ ADD COLUMN "confirmed_time" TEXT,
25
+ ADD COLUMN "buyer_email" TEXT,
26
+ ADD COLUMN "amount_minor" INTEGER,
27
+ ADD COLUMN "currency" TEXT,
28
+ ADD COLUMN "stripe_payment_link_id" TEXT,
29
+ ADD COLUMN "stripe_checkout_session_id" TEXT,
30
+ ADD COLUMN "stripe_invoice_id" TEXT,
31
+ ADD COLUMN "paid_at" TIMESTAMP(3);
32
+
33
+ -- The digest is unique: two rows sharing one is a mint bug, and the constraint
34
+ -- is what turns that bug into a write failure instead of a silent cross-row
35
+ -- credential.
36
+ CREATE UNIQUE INDEX "callbacks_confirm_token_hash_key" ON "callbacks"("confirm_token_hash");
37
+
38
+ CREATE INDEX "callbacks_stripe_payment_link_id_idx" ON "callbacks"("stripe_payment_link_id");
39
+
40
+ CREATE INDEX "callbacks_stripe_invoice_id_idx" ON "callbacks"("stripe_invoice_id");
@@ -0,0 +1,67 @@
1
+ -- #6085 — the Appointment model: a Cal.com booking normalized from the new
2
+ -- webhook receiver (brief H7, spec docs/specs/6085-appointments-from-cal-com-webhooks-spec.md § 4.3).
3
+ --
4
+ -- One new table, three indexes, in the house style of
5
+ -- 20260903010000_add_playbook_instance (CreateTable + CreateIndex immediately
6
+ -- followed by the ADR-0031 rule-3 id-format completion, in ONE migration
7
+ -- since the table is new and needs no retrofit).
8
+ --
9
+ -- The unique index over (site_id, provider, provider_booking_uid) is the
10
+ -- LOAD-BEARING line of this migration: it is the spec's own idempotency key
11
+ -- (§ D4) — a table without it makes "exactly one Appointment row per booking
12
+ -- uid" unenforceable, and § D7(a)'s guarded-write shape races on precisely
13
+ -- this index.
14
+ --
15
+ -- No backfill, no constraint relaxed, no existing table touched, so this
16
+ -- migration carries no zero-row pre-assertion block — there is no way for it
17
+ -- to lose a row.
18
+ --
19
+ -- Rollback: DROP TABLE "appointments". Safe unconditionally while nothing but
20
+ -- #6085's own code reads it.
21
+
22
+ -- CreateTable
23
+ CREATE TABLE "appointments" (
24
+ "id" TEXT NOT NULL DEFAULT ('appt_'::text || (uuidv7())::text),
25
+ "site_id" VARCHAR(200) NOT NULL,
26
+ "provider_booking_uid" VARCHAR(200) NOT NULL,
27
+ "provider" VARCHAR(32) NOT NULL DEFAULT 'cal',
28
+ "start_time" TIMESTAMP(3) NOT NULL,
29
+ "end_time" TIMESTAMP(3) NOT NULL,
30
+ "title" VARCHAR(300),
31
+ "attendee_name" VARCHAR(120) NOT NULL,
32
+ "attendee_email" VARCHAR(254) NOT NULL,
33
+ "attendee_time_zone" VARCHAR(64),
34
+ "status" VARCHAR(32) NOT NULL,
35
+ "paid" BOOLEAN NOT NULL DEFAULT false,
36
+ "payment_initiated_at" TIMESTAMP(3),
37
+ "no_show" BOOLEAN NOT NULL DEFAULT false,
38
+ "cancelled_at" TIMESTAMP(3),
39
+ "lifecycle_event_at" TIMESTAMP(3) NOT NULL,
40
+ "payment_event_at" TIMESTAMP(3),
41
+ "no_show_event_at" TIMESTAMP(3),
42
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
+ "updated_at" TIMESTAMP(3) NOT NULL,
44
+
45
+ CONSTRAINT "appointments_pkey" PRIMARY KEY ("id")
46
+ );
47
+
48
+ -- CreateIndex
49
+ -- The idempotency key (§ D4) — see the file header. `provider` is included
50
+ -- per § 9 SEC-10 so a second scheduling provider's uid space cannot collide
51
+ -- with this one's (#4288).
52
+ CREATE UNIQUE INDEX "appointments_site_id_provider_provider_booking_uid_key" ON "appointments"("site_id", "provider", "provider_booking_uid");
53
+
54
+ -- CreateIndex
55
+ -- The read's `where` + `order by` (§ D6). Without it every appointments page
56
+ -- load sequential-scans "appointments".
57
+ CREATE INDEX "appointments_site_id_start_time_idx" ON "appointments"("site_id", "start_time");
58
+
59
+ -- CreateIndex
60
+ CREATE INDEX "appointments_created_at_idx" ON "appointments"("created_at");
61
+
62
+ -- ADR-0031 rule 3 — appointments: appt_ (registered in
63
+ -- packages/ids/src/registry.ts ENTITY_PREFIX.appointment; this migration's
64
+ -- path is added to migration-sync.test.ts's PK_CHECK_MIGRATIONS).
65
+ ALTER TABLE "appointments" ALTER COLUMN "id" TYPE text COLLATE "C";
66
+ ALTER TABLE "appointments" ALTER COLUMN "id" SET DEFAULT ('appt_'::text || (uuidv7())::text);
67
+ ALTER TABLE "appointments" ADD CONSTRAINT "appointments_id_format" CHECK ("id" ~ '^appt_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
@@ -0,0 +1,70 @@
1
+ -- #6092 — S4's data tier: an asset carries title, description and tags.
2
+ --
3
+ -- Today `assets` holds bytes and a filename and nothing the owner wrote
4
+ -- (decisions.html D10: "an asset has a filename and dimensions only"). The
5
+ -- lightbox edits three things in place — a title, a description and a set of
6
+ -- tags — so the table gains exactly those three columns.
7
+ --
8
+ -- STRICTLY ADDITIVE. Nothing is dropped, renamed, relaxed or backfilled. The
9
+ -- `(workspace_id, key)` identity, the `assets_id_format` and
10
+ -- `assets_workspace_id_format` CHECKs and the `asset_`+UUIDv7 default are all
11
+ -- untouched, so a pre-existing row stays valid and an INSERT written before
12
+ -- this migration keeps working unchanged.
13
+ --
14
+ -- `tags` is TEXT[] with an empty-array default rather than a nullable column:
15
+ -- an untagged asset is an empty set, not an unknown one, and a NULL would give
16
+ -- two spellings of the same state. It carries tag SLUGS, never the human
17
+ -- labels, and it is deliberately UNCONSTRAINED — two vocabularies ride it (the
18
+ -- twelve asset tags and D9's four document tags, since a document is a row in
19
+ -- this same store), and the "fixed set" of D10 is a surface affordance. A tag
20
+ -- whose slug a surface cannot resolve is that surface's problem to render, not
21
+ -- a reason to reject an upload.
22
+ --
23
+ -- `title` and `description` are nullable with no default: NULL means the owner
24
+ -- has not written one, which is distinct from an empty string meaning they
25
+ -- cleared it. `title` is NOT backfilled from `filename` — they are different
26
+ -- facts and the lightbox shows both.
27
+ --
28
+ -- Rollback: ALTER TABLE "assets" DROP COLUMN "tags", DROP COLUMN
29
+ -- "description", DROP COLUMN "title". Safe unconditionally; the columns carry
30
+ -- no constraint and nothing references them.
31
+
32
+ -- The census the post-assertion compares against. ON COMMIT DROP: `prisma
33
+ -- migrate` runs each migration in one transaction, so this disappears with it.
34
+ CREATE TEMP TABLE "_6092_asset_census" ON COMMIT DROP AS
35
+ SELECT count(*) AS n FROM "assets";
36
+
37
+ -- AlterTable
38
+ ALTER TABLE "assets" ADD COLUMN "title" TEXT,
39
+ ADD COLUMN "description" TEXT,
40
+ ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
41
+
42
+ -- ── POST-ASSERTIONS (no row lost; every pre-existing row reads back empty) ──
43
+ -- This is AC-1's second half, asserted against the rows the target database
44
+ -- actually holds rather than against a fixture's.
45
+ DO $$
46
+ DECLARE
47
+ before_n bigint;
48
+ after_n bigint;
49
+ written bigint;
50
+ BEGIN
51
+ SELECT n INTO before_n FROM "_6092_asset_census";
52
+ SELECT count(*) INTO after_n FROM "assets";
53
+ IF before_n <> after_n THEN
54
+ RAISE EXCEPTION
55
+ '#6092 post-assertion: asset count changed across a purely additive migration (% -> %)',
56
+ before_n, after_n;
57
+ END IF;
58
+
59
+ -- Adding the columns must not itself author metadata for anyone. Rows gain a
60
+ -- title, a description or a tag later, one at a time, from the lightbox.
61
+ SELECT count(*) INTO written FROM "assets"
62
+ WHERE "title" IS NOT NULL
63
+ OR "description" IS NOT NULL
64
+ OR "tags" IS DISTINCT FROM ARRAY[]::TEXT[];
65
+ IF written <> 0 THEN
66
+ RAISE EXCEPTION
67
+ '#6092 post-assertion: % pre-existing asset row(s) came out of the migration with metadata already set',
68
+ written;
69
+ END IF;
70
+ END $$;
@@ -0,0 +1,181 @@
1
+ -- #6155 — FlowDefinition and FlowSession gain a workspace isolation key.
2
+ --
3
+ -- The ownership brief (docs/internal/decisions/workspace-owns-data-surfaces-render.md)
4
+ -- and AD-2 (docs/internal/decisions/flow-engine-admin-surface.md): a workspace
5
+ -- owns data, and the flow engine's tenant-scope fence grows to cover it.
6
+ -- `siteId` stays a legacy alias for the public path (unchanged); `workspaceId`
7
+ -- is the isolation key every authenticated read/write will go through
8
+ -- (apps/api/src/lib/tenant-db.ts's new `flows`/`flowSessions` accessors).
9
+ --
10
+ -- NULLABLE, no default (spec D3) — a row whose `siteId` matches no workspace
11
+ -- is left UNSET rather than guessed, and stays invisible to every accessor
12
+ -- rather than visible to the wrong tenant (the same "fails empty, never
13
+ -- wide" posture apps/api/src/services/leads-inbox.ts already takes).
14
+ --
15
+ -- RE-RUNNABLE (NFR3): `ADD COLUMN IF NOT EXISTS` / `CREATE INDEX IF NOT
16
+ -- EXISTS` cover the column and index. Postgres has no
17
+ -- `ADD CONSTRAINT IF NOT EXISTS`, so each format CHECK is wrapped in its own
18
+ -- existence-guarded `DO $$` block — the stated exception to NFR3's "in the
19
+ -- house style of 20260918120000_asset_metadata_columns" note: that
20
+ -- precedent (and 20260713192429_prefixed_uuid_entity_ids's own CHECK
21
+ -- additions) rely on `prisma migrate`'s per-migration transaction and
22
+ -- ledger, not on idempotent DDL — the guard below is this migration's own
23
+ -- addition.
24
+ --
25
+ -- Rollback: DROP INDEX "flow_definitions_workspace_id_idx",
26
+ -- "flow_sessions_workspace_id_idx"; ALTER TABLE "flow_definitions" DROP
27
+ -- CONSTRAINT "flow_definitions_workspace_id_format", DROP COLUMN
28
+ -- "workspace_id"; ALTER TABLE "flow_sessions" DROP CONSTRAINT
29
+ -- "flow_sessions_workspace_id_format", DROP COLUMN "workspace_id". Safe
30
+ -- unconditionally at this pin — nothing reads the column until #6156+.
31
+
32
+ -- The census the post-assertions compare against. ON COMMIT DROP: `prisma
33
+ -- migrate` runs each migration in one transaction, so this disappears with it.
34
+ CREATE TEMP TABLE "_6155_flow_census" ON COMMIT DROP AS
35
+ SELECT (SELECT count(*) FROM "flow_definitions") AS d,
36
+ (SELECT count(*) FROM "flow_sessions") AS s;
37
+
38
+ -- AlterTable
39
+ ALTER TABLE "flow_definitions" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
40
+ ALTER TABLE "flow_sessions" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
41
+
42
+ -- Format CHECKs — NULL-permitting, in the exact shape of the CHECKs
43
+ -- 20260713192429_prefixed_uuid_entity_ids/migration.sql:170-197 already
44
+ -- applies to every other `workspace_id` column. Existence-guarded because
45
+ -- Postgres has no `ADD CONSTRAINT IF NOT EXISTS`.
46
+ DO $$ BEGIN
47
+ IF NOT EXISTS (
48
+ SELECT 1 FROM pg_constraint WHERE conname = 'flow_definitions_workspace_id_format'
49
+ ) THEN
50
+ ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_workspace_id_format"
51
+ CHECK ("workspace_id" IS NULL OR "workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
52
+ END IF;
53
+ END $$;
54
+
55
+ DO $$ BEGIN
56
+ IF NOT EXISTS (
57
+ SELECT 1 FROM pg_constraint WHERE conname = 'flow_sessions_workspace_id_format'
58
+ ) THEN
59
+ ALTER TABLE "flow_sessions" ADD CONSTRAINT "flow_sessions_workspace_id_format"
60
+ CHECK ("workspace_id" IS NULL OR "workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
61
+ END IF;
62
+ END $$;
63
+
64
+ -- CreateIndex
65
+ CREATE INDEX IF NOT EXISTS "flow_definitions_workspace_id_idx" ON "flow_definitions"("workspace_id");
66
+ CREATE INDEX IF NOT EXISTS "flow_sessions_workspace_id_idx" ON "flow_sessions"("workspace_id");
67
+
68
+ -- ── Backfill (spec D2) ──────────────────────────────────────────────────────
69
+ -- Both spellings of the alias, one statement per table. The literal alias
70
+ -- "default" resolves to the seeded sentinel workspace
71
+ -- (database/tenancy.ts DEFAULT_WORKSPACE_ID); every other alias resolves
72
+ -- only by exact workspaces.id equality. An inner join to "workspaces" means a
73
+ -- row whose alias resolves to nothing is simply not updated and stays NULL
74
+ -- — the correct, stated behavior (D3), not an error.
75
+ --
76
+ -- The sentinel id is written as a literal because SQL cannot import
77
+ -- database/tenancy.ts; database/__tests__/tenancy.test.ts pins the same
78
+ -- literal against the exported constant so the two cannot drift.
79
+ UPDATE "flow_definitions" f
80
+ SET "workspace_id" = w."id"
81
+ FROM "workspaces" w
82
+ WHERE f."workspace_id" IS NULL
83
+ AND w."id" = CASE WHEN f."site_id" = 'default'
84
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
85
+ ELSE f."site_id" END;
86
+
87
+ UPDATE "flow_sessions" f
88
+ SET "workspace_id" = w."id"
89
+ FROM "workspaces" w
90
+ WHERE f."workspace_id" IS NULL
91
+ AND w."id" = CASE WHEN f."site_id" = 'default'
92
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
93
+ ELSE f."site_id" END;
94
+
95
+ -- ── POST-ASSERTIONS ─────────────────────────────────────────────────────────
96
+ -- AC-1's own enforcement: nothing lost, nothing guessed, nothing missed that
97
+ -- was resolvable. The fourth check is a NOTICE, not an exception — an
98
+ -- unpinned/unseeded deployment leaving rows NULL is the expected, documented
99
+ -- case (spec § 1.3), never an error this migration should refuse to run over.
100
+ DO $$
101
+ DECLARE
102
+ before_d bigint;
103
+ before_s bigint;
104
+ after_d bigint;
105
+ after_s bigint;
106
+ guessed bigint;
107
+ missed bigint;
108
+ null_d bigint;
109
+ null_s bigint;
110
+ BEGIN
111
+ SELECT d, s INTO before_d, before_s FROM "_6155_flow_census";
112
+ SELECT count(*) INTO after_d FROM "flow_definitions";
113
+ SELECT count(*) INTO after_s FROM "flow_sessions";
114
+
115
+ -- 1. Nothing lost: row counts unchanged on both tables.
116
+ IF before_d <> after_d OR before_s <> after_s THEN
117
+ RAISE EXCEPTION
118
+ '#6155 post-assertion: row count changed across a purely additive migration (flow_definitions % -> %, flow_sessions % -> %)',
119
+ before_d, after_d, before_s, after_s;
120
+ END IF;
121
+
122
+ -- 2. Nothing guessed: no row's workspace_id was set to a value absent
123
+ -- from "workspaces" (the inner-join backfill above cannot produce this
124
+ -- on its own, but a re-run against a database missing the sentinel row
125
+ -- must never leave a dangling reference).
126
+ SELECT count(*) INTO guessed FROM "flow_definitions" f
127
+ WHERE f."workspace_id" IS NOT NULL
128
+ AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = f."workspace_id");
129
+ IF guessed <> 0 THEN
130
+ RAISE EXCEPTION
131
+ '#6155 post-assertion: % flow_definitions row(s) carry a workspace_id absent from workspaces',
132
+ guessed;
133
+ END IF;
134
+ SELECT count(*) INTO guessed FROM "flow_sessions" f
135
+ WHERE f."workspace_id" IS NOT NULL
136
+ AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = f."workspace_id");
137
+ IF guessed <> 0 THEN
138
+ RAISE EXCEPTION
139
+ '#6155 post-assertion: % flow_sessions row(s) carry a workspace_id absent from workspaces',
140
+ guessed;
141
+ END IF;
142
+
143
+ -- 3. Nothing missed: no row whose site_id names a workspace that actually
144
+ -- exists came out of the backfill with workspace_id still NULL.
145
+ SELECT count(*) INTO missed FROM "flow_definitions" f
146
+ WHERE f."workspace_id" IS NULL
147
+ AND EXISTS (
148
+ SELECT 1 FROM "workspaces" w
149
+ WHERE w."id" = CASE WHEN f."site_id" = 'default'
150
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
151
+ ELSE f."site_id" END
152
+ );
153
+ IF missed <> 0 THEN
154
+ RAISE EXCEPTION
155
+ '#6155 post-assertion: % flow_definitions row(s) had a resolvable site_id but came out of the backfill NULL',
156
+ missed;
157
+ END IF;
158
+ SELECT count(*) INTO missed FROM "flow_sessions" f
159
+ WHERE f."workspace_id" IS NULL
160
+ AND EXISTS (
161
+ SELECT 1 FROM "workspaces" w
162
+ WHERE w."id" = CASE WHEN f."site_id" = 'default'
163
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
164
+ ELSE f."site_id" END
165
+ );
166
+ IF missed <> 0 THEN
167
+ RAISE EXCEPTION
168
+ '#6155 post-assertion: % flow_sessions row(s) had a resolvable site_id but came out of the backfill NULL',
169
+ missed;
170
+ END IF;
171
+
172
+ -- 4. A NOTICE, never an exception: an unpinned/unseeded deployment leaving
173
+ -- rows NULL is the expected, documented case (spec § 1.3).
174
+ SELECT count(*) INTO null_d FROM "flow_definitions" WHERE "workspace_id" IS NULL;
175
+ SELECT count(*) INTO null_s FROM "flow_sessions" WHERE "workspace_id" IS NULL;
176
+ IF null_d <> 0 OR null_s <> 0 THEN
177
+ RAISE NOTICE
178
+ '#6155: % flow_definitions row(s) and % flow_sessions row(s) left workspace_id unset — their site_id did not resolve to a workspace this database holds',
179
+ null_d, null_s;
180
+ END IF;
181
+ END $$;