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,172 @@
1
+ -- #6156 — S5b: a magnet is a flow row. FlowDefinition gains four
2
+ -- operator-facing columns — kind, title, landingPage, status — spec D1.
3
+ --
4
+ -- STRICTLY ADDITIVE. Nothing dropped, renamed or relaxed; @@unique
5
+ -- ([siteId, slug]), the `flow_`+UUIDv7 id default, and `listing_id` are all
6
+ -- untouched. The visitor-facing `config` column is byte-identical across
7
+ -- this migration (FR4, asserted below) — these four columns are how the
8
+ -- operator MANAGES a magnet, never what a visitor's session SEES
9
+ -- (AD-3, spec D1/§1.2).
10
+ --
11
+ -- `status` is backfilled 'live' (every row that exists today is already
12
+ -- serving — D1's availability half) and carries NO column default (D1's
13
+ -- publication-safety half, SEC-08): a DEFAULT 'live' would make
14
+ -- PUBLICATION the thing that happens when a future writer says nothing,
15
+ -- which is the exact asymmetry the brief's "a magnet created from the
16
+ -- Admin opens as a draft" commitment forbids. So `status` is added
17
+ -- nullable, backfilled, then SET NOT NULL with no default — every future
18
+ -- writer must name a status or fail to compile.
19
+ --
20
+ -- `title` is NOT NULL with no column default, by the same three-step, for
21
+ -- the positive reason: a nameless magnet must never enter the table
22
+ -- silently. The slug backfills every existing row's title — an honest
23
+ -- placeholder the operator renames.
24
+ --
25
+ -- `kind` KEEPS its column default ('contact') — unlike `status`, a
26
+ -- mis-classified magnet is a mislabel in a list, not a publication or
27
+ -- security control (D1). The backfill below reclassifies the rows D3's
28
+ -- structural rule actually matches; every other row stays the default.
29
+ --
30
+ -- `landing_page` is nullable with NO backfill (D1): a magnet has THREE
31
+ -- public addresses (the indexed landing route, the walk route one segment
32
+ -- deeper, and the results route), so seeding a derived guess would freeze
33
+ -- a route as if it were an operator decision. NULL means "no
34
+ -- operator-declared landing page; the canonical route serves it" — true of
35
+ -- every existing row.
36
+ --
37
+ -- `flow_definitions_status_domain` (D2) is a closed two-value CHECK,
38
+ -- deliberately narrower than the precedent this namespace otherwise follows
39
+ -- (WaitlistExperiment.status, FlowCheckout.status, ProjectListing.status —
40
+ -- all plain String with no CHECK): `status` is what makes D10's
41
+ -- deny-list gate in `resolveSession` SAFE rather than merely convenient —
42
+ -- an unstorable unknown value means "not 'draft'" and "'live'" are the same
43
+ -- set at the database level. No CHECK on `kind`: its vocabulary is six
44
+ -- values and growing (Epic #6025), so a CHECK there would make every new
45
+ -- kind a migration that RELAXES a constraint.
46
+ --
47
+ -- Statement order (D9) is fixed and load-bearing, not incidental: the
48
+ -- `ADD COLUMN` runs FIRST, acquiring ACCESS EXCLUSIVE on `flow_definitions`
49
+ -- before the census is taken, so no concurrent committed write (a listing
50
+ -- approval via `upsertFlowDefinition`, a waitlist-register materialize) can
51
+ -- land between the census and the assertions and falsely trip them. The
52
+ -- whole sequence blocks every read AND write to `flow_definitions` for its
53
+ -- duration — acceptable only because the table is tiny today (§1.4's
54
+ -- census: four seeded presets plus two generated families); a later unit
55
+ -- that grows this table materially owes a re-read of this decision rather
56
+ -- than an inherited assumption.
57
+ --
58
+ -- Not re-runnable by `IF NOT EXISTS` guards (unlike #6155's
59
+ -- 20260919120000_flow_workspace_key, whose NFR3 needed it for a reason
60
+ -- specific to that migration): `prisma migrate`'s per-migration transaction
61
+ -- + the `_prisma_migrations` ledger already make re-runnability a
62
+ -- non-requirement here.
63
+ --
64
+ -- Rollback: ALTER TABLE "flow_definitions" DROP COLUMN "kind", DROP COLUMN
65
+ -- "title", DROP COLUMN "landing_page", DROP COLUMN "status". Safe
66
+ -- unconditionally — dropping "status" drops "flow_definitions_status_domain"
67
+ -- with it, the four columns carry no other constraint, no index and no
68
+ -- relation, and nothing outside this diff references them.
69
+
70
+ -- AlterTable — all four, one statement, taking ACCESS EXCLUSIVE before
71
+ -- anything else in this transaction reads the table (D9).
72
+ ALTER TABLE "flow_definitions"
73
+ ADD COLUMN "kind" TEXT NOT NULL DEFAULT 'contact',
74
+ ADD COLUMN "title" TEXT,
75
+ ADD COLUMN "landing_page" TEXT,
76
+ ADD COLUMN "status" TEXT;
77
+
78
+ -- The census the post-assertions compare against — taken SECOND, under the
79
+ -- lock the ADD COLUMN above already holds, so it is a stable baseline for
80
+ -- every assertion below (D9 / SEC-07). ON COMMIT DROP: `prisma migrate`
81
+ -- runs each migration in one transaction, so this disappears with it.
82
+ CREATE TEMP TABLE "_6156_flow_def_census" ON COMMIT DROP AS
83
+ SELECT count(*) AS n,
84
+ md5(string_agg("config"::text, '' ORDER BY "id")) AS config_hash
85
+ FROM "flow_definitions";
86
+
87
+ -- ── Backfill (D3 / D1) ───────────────────────────────────────────────────
88
+ -- 1. `kind` — D3's structural rule, first match wins. Every row not
89
+ -- matched by a branch below keeps the column default written above.
90
+ UPDATE "flow_definitions"
91
+ SET "kind" = CASE
92
+ WHEN "listing_id" IS NOT NULL THEN 'diagnostic'
93
+ WHEN "config"->>'mode' = 'recommending' THEN 'diagnostic'
94
+ WHEN "config" ? 'deliverables' THEN 'special-report'
95
+ ELSE 'contact'
96
+ END;
97
+
98
+ -- 2. `title` = the slug (an honest placeholder); `status` = 'live' (every
99
+ -- row that exists today is already serving — D1's availability half).
100
+ UPDATE "flow_definitions"
101
+ SET "title" = "slug",
102
+ "status" = 'live';
103
+
104
+ -- SET NOT NULL + the status domain CHECK — AFTER the backfill, so neither
105
+ -- constraint is ever evaluated against a NULL this migration itself wrote.
106
+ ALTER TABLE "flow_definitions" ALTER COLUMN "title" SET NOT NULL;
107
+ ALTER TABLE "flow_definitions" ALTER COLUMN "status" SET NOT NULL;
108
+ ALTER TABLE "flow_definitions"
109
+ ADD CONSTRAINT "flow_definitions_status_domain"
110
+ CHECK ("status" IN ('draft', 'live'));
111
+
112
+ -- ── POST-ASSERTIONS (FR2/FR3/FR4 — AC-1) ────────────────────────────────
113
+ -- Asserted against the rows the target database actually holds, not a
114
+ -- fixture's. A raised exception here rolls the whole migration back
115
+ -- (`prisma migrate`'s per-migration transaction).
116
+ DO $$
117
+ DECLARE
118
+ before_n bigint;
119
+ before_hash text;
120
+ after_n bigint;
121
+ after_hash text;
122
+ null_cols bigint;
123
+ wrong_kind bigint;
124
+ BEGIN
125
+ SELECT n, config_hash INTO before_n, before_hash FROM "_6156_flow_def_census";
126
+ SELECT count(*), md5(string_agg("config"::text, '' ORDER BY "id"))
127
+ INTO after_n, after_hash
128
+ FROM "flow_definitions";
129
+
130
+ -- 1. Nothing lost or gained: row count unchanged across a purely
131
+ -- additive-plus-backfill migration.
132
+ IF before_n <> after_n THEN
133
+ RAISE EXCEPTION
134
+ '#6156 post-assertion: flow_definitions row count changed (% -> %)',
135
+ before_n, after_n;
136
+ END IF;
137
+
138
+ -- 2. `config` is byte-identical (FR4) — this migration writes NO column
139
+ -- other than kind/title/landing_page/status.
140
+ IF before_hash IS DISTINCT FROM after_hash THEN
141
+ RAISE EXCEPTION
142
+ '#6156 post-assertion: flow_definitions.config changed across a migration that must not touch it';
143
+ END IF;
144
+
145
+ -- 3. Every pre-existing row leaves with a non-NULL status/title/kind
146
+ -- (landing_page is NOT asserted — D1: it is deliberately unbackfilled).
147
+ SELECT count(*) INTO null_cols FROM "flow_definitions"
148
+ WHERE "status" IS NULL OR "title" IS NULL OR "kind" IS NULL;
149
+ IF null_cols <> 0 THEN
150
+ RAISE EXCEPTION
151
+ '#6156 post-assertion: % flow_definitions row(s) came out of the backfill with a NULL status/title/kind',
152
+ null_cols;
153
+ END IF;
154
+
155
+ -- 4. Backfill rule correctness (D3) — every listing-projected row, and
156
+ -- every recommending-mode row, classified 'diagnostic'.
157
+ SELECT count(*) INTO wrong_kind FROM "flow_definitions"
158
+ WHERE "listing_id" IS NOT NULL AND "kind" <> 'diagnostic';
159
+ IF wrong_kind <> 0 THEN
160
+ RAISE EXCEPTION
161
+ '#6156 post-assertion: % flow_definitions row(s) with listing_id set did not classify as diagnostic',
162
+ wrong_kind;
163
+ END IF;
164
+
165
+ SELECT count(*) INTO wrong_kind FROM "flow_definitions"
166
+ WHERE "config"->>'mode' = 'recommending' AND "kind" <> 'diagnostic';
167
+ IF wrong_kind <> 0 THEN
168
+ RAISE EXCEPTION
169
+ '#6156 post-assertion: % flow_definitions row(s) with config.mode = recommending did not classify as diagnostic',
170
+ wrong_kind;
171
+ END IF;
172
+ END $$;
@@ -0,0 +1,41 @@
1
+ -- #6183 (S9b) — the WorkspaceMeetings model: the operator's enabled-slug
2
+ -- gate for meeting types (spec docs/specs/6183-s9b-enabled-meeting-slugs-live-spec.md
3
+ -- D1, D11). A fourth workspace site-config blob, structurally identical to
4
+ -- its three siblings (workspace_themes / workspace_site_identities /
5
+ -- workspace_seos) — same `@unique(workspace_id)` key, same Json-column
6
+ -- convention, validation happens at the API layer.
7
+ --
8
+ -- One new table, one unique index, in the house style of
9
+ -- 20260918120000_add_appointment (CreateTable + CreateIndex immediately
10
+ -- followed by the ADR-0031 rule-3 id-format completion, in ONE migration
11
+ -- since the table is new and needs no retrofit). No `workspace_id_format`
12
+ -- CHECK, matching that same precedent — the baseline migration's 12
13
+ -- relation-free scope checks are pinned to it alone
14
+ -- (packages/ids/src/__tests__/migration-sync.test.ts), and no per-entity
15
+ -- migration since has added one.
16
+ --
17
+ -- No backfill, no constraint relaxed, no existing table touched.
18
+ --
19
+ -- Rollback: DROP TABLE "workspace_meetings". Safe unconditionally while
20
+ -- nothing but #6183's own code reads it.
21
+
22
+ -- CreateTable
23
+ CREATE TABLE "workspace_meetings" (
24
+ "id" TEXT NOT NULL DEFAULT ('meet_'::text || (uuidv7())::text),
25
+ "workspace_id" TEXT NOT NULL,
26
+ "data" JSONB NOT NULL,
27
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
28
+ "updated_at" TIMESTAMPTZ NOT NULL,
29
+
30
+ CONSTRAINT "workspace_meetings_pkey" PRIMARY KEY ("id")
31
+ );
32
+
33
+ -- CreateIndex
34
+ CREATE UNIQUE INDEX "workspace_meetings_workspace_id_key" ON "workspace_meetings"("workspace_id");
35
+
36
+ -- ADR-0031 rule 3 — workspace_meetings: meet_ (registered in
37
+ -- packages/ids/src/registry.ts ENTITY_PREFIX.workspaceMeetings; this
38
+ -- migration's path is added to migration-sync.test.ts's PK_CHECK_MIGRATIONS).
39
+ ALTER TABLE "workspace_meetings" ALTER COLUMN "id" TYPE text COLLATE "C";
40
+ ALTER TABLE "workspace_meetings" ALTER COLUMN "id" SET DEFAULT ('meet_'::text || (uuidv7())::text);
41
+ ALTER TABLE "workspace_meetings" ADD CONSTRAINT "workspace_meetings_id_format" CHECK ("id" ~ '^meet_[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,185 @@
1
+ -- #6203 — ProjectListing and Lead gain a workspace isolation key.
2
+ --
3
+ -- The ownership brief (docs/internal/decisions/workspace-owns-data-surfaces-render.md)
4
+ -- and AD-4 (docs/internal/decisions/flow-engine-admin-surface.md): a workspace
5
+ -- owns data, and the Inbox's tenant-scope fence grows to cover the two
6
+ -- models it reads. `siteId` stays a legacy alias for the anonymous write
7
+ -- paths (unchanged); `workspaceId` is the isolation key every Admin read
8
+ -- will go through (apps/api/src/lib/tenant-db.ts's new
9
+ -- `projectListings`/`leads` accessors). Mirrors
10
+ -- database/migrations/20260919120000_flow_workspace_key step for step
11
+ -- (#6155's precedent for FlowDefinition/FlowSession).
12
+ --
13
+ -- NULLABLE, no default (spec D2) — a row whose `siteId` matches no workspace
14
+ -- is left UNSET rather than guessed, and stays invisible to every accessor
15
+ -- rather than visible to the wrong tenant (the same "fails empty, never
16
+ -- wide" posture apps/api/src/services/leads-inbox.ts already takes).
17
+ --
18
+ -- RE-RUNNABLE (NFR3): `ADD COLUMN IF NOT EXISTS` / `CREATE INDEX IF NOT
19
+ -- EXISTS` cover the column and index. Postgres has no
20
+ -- `ADD CONSTRAINT IF NOT EXISTS`, so each format CHECK is wrapped in its own
21
+ -- existence-guarded `DO $$` block, exactly as the #6155 precedent does.
22
+ --
23
+ -- Rollback: DROP INDEX "project_listings_workspace_id_idx",
24
+ -- "leads_workspace_id_idx"; ALTER TABLE "project_listings" DROP
25
+ -- CONSTRAINT "project_listings_workspace_id_format", DROP COLUMN
26
+ -- "workspace_id"; ALTER TABLE "leads" DROP CONSTRAINT
27
+ -- "leads_workspace_id_format", DROP COLUMN "workspace_id". Safe
28
+ -- unconditionally at this pin — nothing reads the column until #6204+.
29
+
30
+ -- The census the post-assertions compare against. ON COMMIT DROP: `prisma
31
+ -- migrate` runs each migration in one transaction, so this disappears with it.
32
+ CREATE TEMP TABLE "_6203_inbox_census" ON COMMIT DROP AS
33
+ SELECT (SELECT count(*) FROM "project_listings") AS p,
34
+ (SELECT count(*) FROM "leads") AS l;
35
+
36
+ -- AlterTable
37
+ ALTER TABLE "project_listings" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
38
+ ALTER TABLE "leads" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
39
+
40
+ -- Format CHECKs — NULL-permitting, in the exact shape of the CHECKs
41
+ -- 20260713192429_prefixed_uuid_entity_ids/migration.sql already applies to
42
+ -- every other `workspace_id` column. Existence-guarded because Postgres has
43
+ -- no `ADD CONSTRAINT IF NOT EXISTS`. This is what makes the Lead.siteId
44
+ -- provenance gap (spec §1.3, §9 SEC-1) non-exploitable as an id-SHAPE
45
+ -- attack: a site_id that is not a well-formed wksp_<uuidv7> cannot land in
46
+ -- this column at all.
47
+ DO $$ BEGIN
48
+ IF NOT EXISTS (
49
+ SELECT 1 FROM pg_constraint WHERE conname = 'project_listings_workspace_id_format'
50
+ ) THEN
51
+ ALTER TABLE "project_listings" ADD CONSTRAINT "project_listings_workspace_id_format"
52
+ 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}$');
53
+ END IF;
54
+ END $$;
55
+
56
+ DO $$ BEGIN
57
+ IF NOT EXISTS (
58
+ SELECT 1 FROM pg_constraint WHERE conname = 'leads_workspace_id_format'
59
+ ) THEN
60
+ ALTER TABLE "leads" ADD CONSTRAINT "leads_workspace_id_format"
61
+ 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}$');
62
+ END IF;
63
+ END $$;
64
+
65
+ -- CreateIndex
66
+ CREATE INDEX IF NOT EXISTS "project_listings_workspace_id_idx" ON "project_listings"("workspace_id");
67
+ CREATE INDEX IF NOT EXISTS "leads_workspace_id_idx" ON "leads"("workspace_id");
68
+
69
+ -- ── Backfill (spec D2, §5.4) ────────────────────────────────────────────────
70
+ -- Both spellings of the alias, one statement per table. The literal alias
71
+ -- "default" resolves to the seeded sentinel workspace
72
+ -- (database/tenancy.ts DEFAULT_WORKSPACE_ID); every other alias resolves
73
+ -- only by exact workspaces.id equality. An inner join to "workspaces" means a
74
+ -- row whose alias resolves to nothing is simply not updated and stays NULL
75
+ -- — the correct, stated behavior (D2), not an error.
76
+ --
77
+ -- The sentinel id is a literal because SQL cannot import
78
+ -- database/tenancy.ts. It is ADR 0031's reserved-range sentinel and changes
79
+ -- only with that ADR — no test pins it against SQL (#6155's own migration
80
+ -- carries the identical correction: the sibling tenancy.test.ts compares
81
+ -- only the imported constant, not this SQL literal).
82
+ UPDATE "project_listings" p
83
+ SET "workspace_id" = w."id"
84
+ FROM "workspaces" w
85
+ WHERE p."workspace_id" IS NULL
86
+ AND w."id" = CASE WHEN p."site_id" = 'default'
87
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
88
+ ELSE p."site_id" END;
89
+
90
+ UPDATE "leads" l
91
+ SET "workspace_id" = w."id"
92
+ FROM "workspaces" w
93
+ WHERE l."workspace_id" IS NULL
94
+ AND w."id" = CASE WHEN l."site_id" = 'default'
95
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
96
+ ELSE l."site_id" END;
97
+
98
+ -- ── POST-ASSERTIONS ─────────────────────────────────────────────────────────
99
+ -- AC-1's own enforcement: nothing lost, nothing guessed, nothing missed that
100
+ -- was resolvable. The fourth check is a NOTICE, not an exception — an
101
+ -- unpinned/unseeded deployment leaving rows NULL is the expected, documented
102
+ -- case (spec §1.9, NFR4), never an error this migration should refuse to
103
+ -- run over.
104
+ DO $$
105
+ DECLARE
106
+ before_p bigint;
107
+ before_l bigint;
108
+ after_p bigint;
109
+ after_l bigint;
110
+ guessed bigint;
111
+ missed bigint;
112
+ null_p bigint;
113
+ null_l bigint;
114
+ BEGIN
115
+ SELECT p, l INTO before_p, before_l FROM "_6203_inbox_census";
116
+ SELECT count(*) INTO after_p FROM "project_listings";
117
+ SELECT count(*) INTO after_l FROM "leads";
118
+
119
+ -- 1. Nothing lost: row counts unchanged on both tables.
120
+ IF before_p <> after_p OR before_l <> after_l THEN
121
+ RAISE EXCEPTION
122
+ '#6203 post-assertion: row count changed across a purely additive migration (project_listings % -> %, leads % -> %)',
123
+ before_p, after_p, before_l, after_l;
124
+ END IF;
125
+
126
+ -- 2. Nothing guessed: no row's workspace_id was set to a value absent
127
+ -- from "workspaces" (the inner-join backfill above cannot produce this
128
+ -- on its own, but a re-run against a database missing the sentinel row
129
+ -- must never leave a dangling reference).
130
+ SELECT count(*) INTO guessed FROM "project_listings" p
131
+ WHERE p."workspace_id" IS NOT NULL
132
+ AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = p."workspace_id");
133
+ IF guessed <> 0 THEN
134
+ RAISE EXCEPTION
135
+ '#6203 post-assertion: % project_listings row(s) carry a workspace_id absent from workspaces',
136
+ guessed;
137
+ END IF;
138
+ SELECT count(*) INTO guessed FROM "leads" l
139
+ WHERE l."workspace_id" IS NOT NULL
140
+ AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = l."workspace_id");
141
+ IF guessed <> 0 THEN
142
+ RAISE EXCEPTION
143
+ '#6203 post-assertion: % leads row(s) carry a workspace_id absent from workspaces',
144
+ guessed;
145
+ END IF;
146
+
147
+ -- 3. Nothing missed: no row whose site_id names a workspace that actually
148
+ -- exists came out of the backfill with workspace_id still NULL.
149
+ SELECT count(*) INTO missed FROM "project_listings" p
150
+ WHERE p."workspace_id" IS NULL
151
+ AND EXISTS (
152
+ SELECT 1 FROM "workspaces" w
153
+ WHERE w."id" = CASE WHEN p."site_id" = 'default'
154
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
155
+ ELSE p."site_id" END
156
+ );
157
+ IF missed <> 0 THEN
158
+ RAISE EXCEPTION
159
+ '#6203 post-assertion: % project_listings row(s) had a resolvable site_id but came out of the backfill NULL',
160
+ missed;
161
+ END IF;
162
+ SELECT count(*) INTO missed FROM "leads" l
163
+ WHERE l."workspace_id" IS NULL
164
+ AND EXISTS (
165
+ SELECT 1 FROM "workspaces" w
166
+ WHERE w."id" = CASE WHEN l."site_id" = 'default'
167
+ THEN 'wksp_00000000-0000-7000-8000-000000000001'
168
+ ELSE l."site_id" END
169
+ );
170
+ IF missed <> 0 THEN
171
+ RAISE EXCEPTION
172
+ '#6203 post-assertion: % leads row(s) had a resolvable site_id but came out of the backfill NULL',
173
+ missed;
174
+ END IF;
175
+
176
+ -- 4. A NOTICE, never an exception: an unpinned/unseeded deployment leaving
177
+ -- rows NULL is the expected, documented case (spec §1.9).
178
+ SELECT count(*) INTO null_p FROM "project_listings" WHERE "workspace_id" IS NULL;
179
+ SELECT count(*) INTO null_l FROM "leads" WHERE "workspace_id" IS NULL;
180
+ IF null_p <> 0 OR null_l <> 0 THEN
181
+ RAISE NOTICE
182
+ '#6203: % project_listings row(s) and % leads row(s) left workspace_id unset — their site_id did not resolve to a workspace this database holds',
183
+ null_p, null_l;
184
+ END IF;
185
+ END $$;
@@ -0,0 +1,70 @@
1
+ -- #6204 — Lead gains a nullable triage stamp.
2
+ --
3
+ -- Operator decision Q1(a) (#6026, 2026-09-18): a message is a lead that needs
4
+ -- an answer — same capture path, same routing, same tenancy, different
5
+ -- follow-up. One nullable column beats a second entity, and
6
+ -- docs/internal/decisions/flow-engine-admin-surface.md AD-4 stays true: the
7
+ -- Inbox is a READ PROJECTION over tables that already hold decidable state,
8
+ -- never a new decision table.
9
+ --
10
+ -- PURELY ADDITIVE (spec NFR1): two ADD COLUMNs, one CREATE INDEX. No UPDATE, no
11
+ -- backfill, no DEFAULT, no NOT NULL — every existing row keeps NULL, which is
12
+ -- exactly "unhandled" (spec D5, AC-1). There is no data transformation here, so
13
+ -- this migration carries none of the census/post-assertion apparatus
14
+ -- 20260919140000_inbox_workspace_key needed to prove ITS backfill correct.
15
+ --
16
+ -- RE-RUNNABLE (spec NFR2): IF NOT EXISTS on every statement, the house style of
17
+ -- 20260919140000_inbox_workspace_key/migration.sql.
18
+ --
19
+ -- ORDER-SAFE (spec NFR2, security finding S-9): the index references
20
+ -- "workspace_id", which 20260919140000_inbox_workspace_key adds — a migration
21
+ -- this one sorts strictly after (20260920 > 20260919), so in the normal case
22
+ -- the guard below is a formality. It stays a guard rather than an assumption:
23
+ -- `IF NOT EXISTS` guards the INDEX's own name, not the column it references,
24
+ -- so an unguarded CREATE INDEX would abort inside prisma migrate's
25
+ -- per-migration transaction and wedge the ledger for the whole deployment if
26
+ -- that migration were ever skipped, renamed, or rolled back on a given
27
+ -- database. The two ADD COLUMNs above it are unconditional because they
28
+ -- reference nothing another migration owns.
29
+ --
30
+ -- CREATE INDEX is NOT CONCURRENT (security finding S-11), and cannot be:
31
+ -- CONCURRENTLY may not run inside prisma migrate's per-migration transaction.
32
+ -- It therefore takes a lock on "leads" for its duration, which blocks the three
33
+ -- anonymous capture routes (a write failure there surfaces as 503
34
+ -- persist_failed, routes/leads/index.ts), and every subsequent insert
35
+ -- pays a fourth index write. Accepted: "leads" carries four indexes already
36
+ -- (siteId, createdAt, email, workspaceId) and is small at this pin. A
37
+ -- deployment with a large leads table should create this index out-of-band
38
+ -- with CONCURRENTLY first — the IF NOT EXISTS guard then makes this
39
+ -- statement a no-op.
40
+ --
41
+ -- Rollback: DROP INDEX "leads_workspace_id_handled_at_idx";
42
+ -- ALTER TABLE "leads" DROP COLUMN "handled_at", DROP COLUMN "handled_by".
43
+ -- Safe ONLY at this pin, and the qualifier is load-bearing: once the Needs-you
44
+ -- section ships, dropping these columns destroys operator triage state. After
45
+ -- that point the rollback is a forward migration, not a DROP.
46
+
47
+ -- AlterTable
48
+ ALTER TABLE "leads" ADD COLUMN IF NOT EXISTS "handled_at" TIMESTAMP(3);
49
+ -- The actor (spec D4, security finding S-7) — the sibling of
50
+ -- project_listings."decided_by". Plain TEXT, no FK:
51
+ -- ADR 0009 D2 forbids a relation crossing from inbox into auth.
52
+ ALTER TABLE "leads" ADD COLUMN IF NOT EXISTS "handled_by" TEXT;
53
+
54
+ -- CreateIndex — the read is
55
+ -- workspace_id = $1 AND handled_at IS NULL AND source = ANY($2)
56
+ -- ordered by created_at desc. A two-column (workspace_id, handled_at) index is
57
+ -- the one that serves it: it narrows to the tenant and then to the unhandled
58
+ -- partition, which is the small side and the side that shrinks as an operator
59
+ -- works the queue. `source` is deliberately NOT a third column (spec D10).
60
+ DO $$ BEGIN
61
+ IF EXISTS (
62
+ SELECT 1 FROM information_schema.columns
63
+ WHERE table_name = 'leads' AND column_name = 'workspace_id'
64
+ ) THEN
65
+ CREATE INDEX IF NOT EXISTS "leads_workspace_id_handled_at_idx"
66
+ ON "leads"("workspace_id", "handled_at");
67
+ ELSE
68
+ RAISE NOTICE '#6204: leads.workspace_id absent — index skipped. 20260919140000_inbox_workspace_key has not run against this database; the Needs-you read cannot be served until it does (spec § 10.1).';
69
+ END IF;
70
+ END $$;
@@ -0,0 +1,57 @@
1
+ -- #6246 — Lead gains a second workspace-scoped composite index, this one
2
+ -- for the per-magnet Signups/Subscribers/Messages read.
3
+ --
4
+ -- The read is workspace_id = $1 AND source = $2, ordered by created_at desc
5
+ -- (spec § 1.5/D1) — an EXACT-EQUALITY match on `source`, distinct from
6
+ -- 20260920120000_lead_handled_at's own (workspace_id, handled_at)
7
+ -- composite, which serves an `IN`-over-a-four-value-allowlist filter. The
8
+ -- two indexes serve two different query shapes over the same two-column
9
+ -- narrowing pattern; neither can stand in for the other.
10
+ --
11
+ -- PURELY ADDITIVE (mirrors 20260920120000_lead_handled_at's own NFR1): one
12
+ -- CREATE INDEX, no column, no backfill, no data transformation — so this
13
+ -- migration carries none of the census/post-assertion apparatus
14
+ -- 20260919140000_inbox_workspace_key needed to prove ITS backfill correct.
15
+ --
16
+ -- RE-RUNNABLE (mirrors the same precedent's NFR2): IF NOT EXISTS on the
17
+ -- index, guarded on the referenced column's presence.
18
+ --
19
+ -- ORDER-SAFE: the index references "workspace_id", which
20
+ -- 20260919140000_inbox_workspace_key adds — a migration this one sorts
21
+ -- strictly after (20260920130000 > 20260919140000), so in the normal case
22
+ -- the guard below is a formality, kept for the same reason its precedent
23
+ -- keeps one: IF NOT EXISTS guards the INDEX's own name, not the column it
24
+ -- references, so an unguarded CREATE INDEX would abort inside prisma
25
+ -- migrate's per-migration transaction and wedge the ledger for the whole
26
+ -- deployment if that migration were ever skipped, renamed, or rolled back
27
+ -- on a given database.
28
+ --
29
+ -- CREATE INDEX is NOT CONCURRENT, and cannot be: CONCURRENTLY may not run
30
+ -- inside prisma migrate's per-migration transaction. It therefore takes a
31
+ -- lock on "leads" for its duration, the SAME accepted risk
32
+ -- 20260920120000_lead_handled_at's own header names — "leads" carries five
33
+ -- indexes already (siteId, createdAt, email, workspaceId,
34
+ -- (workspaceId, handledAt)) and is small at this pin. A deployment with a
35
+ -- large leads table should create this index out-of-band with
36
+ -- CONCURRENTLY first — the IF NOT EXISTS guard then makes this statement a
37
+ -- no-op.
38
+ --
39
+ -- Rollback: DROP INDEX "leads_workspace_id_source_idx";
40
+ -- Safe at any point — this migration adds no column and no application
41
+ -- state depends on the index existing, only on its absence being slower.
42
+
43
+ -- CreateIndex — the read is workspace_id = $1 AND source = $2, ordered by
44
+ -- created_at desc (spec § 1.5/D1). A two-column (workspace_id, source)
45
+ -- index serves it directly: it narrows to the tenant and then to the
46
+ -- exact-match magnet source in one pass.
47
+ DO $$ BEGIN
48
+ IF EXISTS (
49
+ SELECT 1 FROM information_schema.columns
50
+ WHERE table_name = 'leads' AND column_name = 'workspace_id'
51
+ ) THEN
52
+ CREATE INDEX IF NOT EXISTS "leads_workspace_id_source_idx"
53
+ ON "leads"("workspace_id", "source");
54
+ ELSE
55
+ RAISE NOTICE '#6246: leads.workspace_id absent — index skipped. 20260919140000_inbox_workspace_key has not run against this database; the per-magnet list read cannot be served efficiently until it does.';
56
+ END IF;
57
+ END $$;
@@ -0,0 +1,68 @@
1
+ -- #6310 D8/D15 — DeliverableGrant gains its own `workspace_id`.
2
+ --
3
+ -- The one migration this spec carries — it moves no `DeliverableAsset` row
4
+ -- (D3: existing rows keep delivering, unmigrated; AC3's grounding is in the
5
+ -- spec, not here). It restores the serve-time tenancy re-check the legacy
6
+ -- arm already has (`asset.siteId !== grant.siteId`, routes/deliverables/index.ts)
7
+ -- for the new `Asset`-backed grant class: `workspaceId` is written at mint,
8
+ -- from the SAME value that authorized the mint-time resolution (D15), so the
9
+ -- serve read is `{ id, workspaceId }` rather than an unscoped `{ id }`.
10
+ --
11
+ -- NULLABLE, no default — mirrors database/migrations/20260919140000_inbox_workspace_key's
12
+ -- idiom step for step: a value this migration cannot derive (every grant
13
+ -- that exists at migration time is a legacy `dasset_`/`playbook:` grant
14
+ -- whose serve arm never reads this column) is left UNSET rather than
15
+ -- guessed. NO BACKFILL: there is nothing to backfill — the column has no
16
+ -- source column to derive from (unlike site_id -> workspace_id, this is a
17
+ -- wholly new fact, captured only at a FUTURE mint).
18
+ --
19
+ -- RE-RUNNABLE: `ADD COLUMN IF NOT EXISTS` covers the column; the format
20
+ -- CHECK is wrapped in an existence-guarded `DO $$` block since Postgres has
21
+ -- no `ADD CONSTRAINT IF NOT EXISTS`. No index — the column is read only by
22
+ -- `findFirst`-by-id, never filtered on.
23
+ --
24
+ -- Rollback: ALTER TABLE "deliverable_grants" DROP CONSTRAINT
25
+ -- "deliverable_grants_workspace_id_format", DROP COLUMN "workspace_id".
26
+ -- Safe unconditionally at this pin — no `asset_`-class grant exists yet to
27
+ -- lose data from.
28
+
29
+ -- The census the post-assertion compares against. ON COMMIT DROP: `prisma
30
+ -- migrate` runs each migration in one transaction, so this disappears with it.
31
+ CREATE TEMP TABLE "_6310_deliverable_grants_census" ON COMMIT DROP AS
32
+ SELECT count(*) AS n FROM "deliverable_grants";
33
+
34
+ -- AlterTable
35
+ ALTER TABLE "deliverable_grants" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
36
+
37
+ -- Format CHECK — NULL-permitting, in the exact shape of the CHECKs
38
+ -- 20260713192429_prefixed_uuid_entity_ids/migration.sql already applies to
39
+ -- every other `workspace_id` column (mirrored verbatim from #6203's own
40
+ -- migration). Existence-guarded because Postgres has no
41
+ -- `ADD CONSTRAINT IF NOT EXISTS`.
42
+ DO $$ BEGIN
43
+ IF NOT EXISTS (
44
+ SELECT 1 FROM pg_constraint WHERE conname = 'deliverable_grants_workspace_id_format'
45
+ ) THEN
46
+ ALTER TABLE "deliverable_grants" ADD CONSTRAINT "deliverable_grants_workspace_id_format"
47
+ 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}$');
48
+ END IF;
49
+ END $$;
50
+
51
+ -- ── POST-ASSERTION ──────────────────────────────────────────────────────────
52
+ -- NFR1's own enforcement: exactly one nullable column added, no row moved or
53
+ -- rewritten. No "nothing guessed"/"nothing missed" checks (unlike #6203) —
54
+ -- there is no backfill for those checks to verify.
55
+ DO $$
56
+ DECLARE
57
+ before_n bigint;
58
+ after_n bigint;
59
+ BEGIN
60
+ SELECT n INTO before_n FROM "_6310_deliverable_grants_census";
61
+ SELECT count(*) INTO after_n FROM "deliverable_grants";
62
+
63
+ IF before_n <> after_n THEN
64
+ RAISE EXCEPTION
65
+ '#6310 post-assertion: row count changed across a purely additive migration (deliverable_grants % -> %)',
66
+ before_n, after_n;
67
+ END IF;
68
+ END $$;