create-ailk 0.24.0 → 0.25.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 (599) hide show
  1. package/README.md +42 -20
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +4 -2
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +31 -3
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +65 -29
  10. package/dist/lib/design-checks.d.ts +9 -4
  11. package/dist/lib/design-checks.js +13 -7
  12. package/dist/lib/extract-module-bundle.js +29 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +1 -1
  14. package/dist/lib/fetch-template-snapshot.js +7 -3
  15. package/dist/lib/import-graph.d.ts +1 -1
  16. package/dist/lib/import-graph.js +4 -4
  17. package/dist/lib/module-license-gate.d.ts +12 -1
  18. package/dist/lib/module-license-gate.js +34 -12
  19. package/dist/module-architecture.d.ts +20 -3
  20. package/dist/module-architecture.js +117 -550
  21. package/dist/paid-sections.d.ts +25 -1
  22. package/dist/paid-sections.js +88 -3
  23. package/dist/surfaces.js +10 -2
  24. package/dist/template-record.d.ts +14 -4
  25. package/dist/template-record.js +23 -5
  26. package/dist/topology.d.ts +16 -3
  27. package/dist/topology.js +31 -19
  28. package/dist/update.d.ts +10 -1
  29. package/dist/update.js +540 -16
  30. package/package.json +3 -3
  31. package/release-scope.json +1 -1
  32. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  33. package/templates/.claude/hooks/template-guard.sh +137 -0
  34. package/templates/.claude/rules/extending.md +106 -0
  35. package/templates/.claude/rules/loop-contract.md +2 -2
  36. package/templates/.claude/settings.json +11 -0
  37. package/templates/.env.example +5 -5
  38. package/templates/.github/workflows/ci.yml +7 -4
  39. package/templates/CLAUDE.md +11 -1
  40. package/templates/CONVENTIONS.md +8 -10
  41. package/templates/README.md +14 -13
  42. package/templates/apps/api/.env.example +2 -2
  43. package/templates/apps/api/CLAUDE.md +102 -76
  44. package/templates/apps/api/jest.config.cjs +15 -5
  45. package/templates/apps/api/package.json +16 -20
  46. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  47. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  48. package/templates/apps/api/src/index.ts +1 -1
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
  50. package/templates/apps/api/src/modules/commerce.ts +10 -0
  51. package/templates/apps/api/src/modules/content-write.ts +10 -0
  52. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  53. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  54. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  55. package/templates/apps/api/src/modules/workspace.ts +10 -0
  56. package/templates/apps/api/src/server.ts +117 -1882
  57. package/templates/apps/api/vercel.json +2 -2
  58. package/templates/apps/mcp/CLAUDE.md +67 -20
  59. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  60. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  61. package/templates/apps/mcp/jest.config.cjs +6 -1
  62. package/templates/apps/mcp/package.json +5 -13
  63. package/templates/apps/mcp/src/app.ts +8 -7
  64. package/templates/apps/mcp/src/index.ts +6 -34
  65. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  66. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  67. package/templates/apps/mcp/src/site.ts +12 -0
  68. package/templates/apps/web/.env.example +1 -1
  69. package/templates/apps/web/CLAUDE.md +10 -3
  70. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  71. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  72. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  73. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
  74. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  75. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  76. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  77. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  78. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  79. package/templates/apps/web/auth.ts +2 -2
  80. package/templates/apps/web/jest.config.cjs +1 -0
  81. package/templates/apps/web/lib/auth-email.ts +1 -1
  82. package/templates/apps/web/lib/relay.ts +2 -2
  83. package/templates/apps/web/lib/release-scope.ts +47 -0
  84. package/templates/apps/web/lib/server-api.ts +1 -1
  85. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  86. package/templates/customizations.yaml +37 -0
  87. package/templates/database/prisma.config.ts +10 -14
  88. package/templates/database/seed-test.ts +2 -2
  89. package/templates/database/seed.ts +5 -5
  90. package/templates/package.json +12 -6
  91. package/templates/pnpm-workspace.yaml +0 -1
  92. package/templates/project.yaml +16 -8
  93. package/templates/tools/jest/reporters.cjs +65 -0
  94. package/templates/tools/lint-architecture.ts +553 -7
  95. package/templates/tsconfig.json +1 -2
  96. package/versions.json +48 -35
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  98. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  99. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  100. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  101. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  102. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  103. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  104. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  105. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  106. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  107. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  108. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  109. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  110. package/templates/apps/api/src/bin/spokes.ts +0 -198
  111. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  112. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  113. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  114. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  115. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  116. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  117. package/templates/apps/api/src/config/index.ts +0 -596
  118. package/templates/apps/api/src/config/modules.ts +0 -83
  119. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  120. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  121. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  122. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  123. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  124. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  125. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  126. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  127. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  128. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  129. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  130. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  131. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  132. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  133. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  134. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  135. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  136. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  137. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  138. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
  139. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  140. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  141. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  142. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  143. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  144. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  145. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  146. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  147. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  148. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  149. package/templates/apps/api/src/lib/errors.ts +0 -95
  150. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  151. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  152. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  153. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  154. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  155. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  156. package/templates/apps/api/src/lib/locales.ts +0 -201
  157. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  158. package/templates/apps/api/src/lib/prisma.ts +0 -21
  159. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  160. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  161. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  162. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  163. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  164. package/templates/apps/api/src/lib/site.ts +0 -257
  165. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  166. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  167. package/templates/apps/api/src/lib/stripe.ts +0 -321
  168. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  169. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  170. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  171. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  172. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  173. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  174. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  175. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  176. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  177. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  178. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  179. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  180. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  181. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  182. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  183. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  184. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  185. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  186. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  187. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  188. package/templates/apps/api/src/middleware/auth.ts +0 -864
  189. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  190. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  191. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
  192. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  193. package/templates/apps/api/src/openapi/index.ts +0 -18
  194. package/templates/apps/api/src/openapi/spec.ts +0 -2148
  195. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  196. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  197. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  198. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  199. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  200. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  201. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  202. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  203. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  204. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  205. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  206. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  207. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  208. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
  209. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  210. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
  211. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  212. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  213. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  214. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  215. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  216. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  217. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  218. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  219. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  220. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  221. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  222. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  223. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  224. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  225. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  226. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  227. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  228. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  229. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  230. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  231. package/templates/apps/api/src/routes/content/get-page.ts +0 -183
  232. package/templates/apps/api/src/routes/content/index.ts +0 -49
  233. package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
  234. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  235. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  236. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  237. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  238. package/templates/apps/api/src/routes/content/search.ts +0 -100
  239. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  240. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  241. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  242. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  243. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  244. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  245. package/templates/apps/api/src/routes/leads/index.ts +0 -639
  246. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  247. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  248. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  249. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  250. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
  251. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
  252. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  253. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  254. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  255. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  256. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  257. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  258. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  259. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  260. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  261. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  262. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  263. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  264. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  265. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  266. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  267. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  268. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  269. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  270. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  271. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  272. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  273. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  274. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
  275. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  276. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  277. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  278. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  279. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  280. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  281. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  282. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  285. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  286. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  287. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
  288. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
  289. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  290. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  291. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  292. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  293. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  294. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  295. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  296. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
  297. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  298. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  299. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  300. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  301. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  302. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  303. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  304. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  305. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  306. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
  307. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
  308. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
  309. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
  310. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  311. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  312. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  313. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  314. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  315. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  316. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  317. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  318. package/templates/apps/api/src/services/abandon-email.ts +0 -336
  319. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  320. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  321. package/templates/apps/api/src/services/audit-log.ts +0 -961
  322. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  323. package/templates/apps/api/src/services/consent.ts +0 -834
  324. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  325. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
  326. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  327. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  328. package/templates/apps/api/src/services/entitlements.ts +0 -338
  329. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  330. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  331. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  332. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  333. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  334. package/templates/apps/api/src/services/listing-config.ts +0 -178
  335. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  336. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
  337. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  338. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
  339. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  340. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
  341. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
  342. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  343. package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
  344. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
  345. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  346. package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
  347. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  348. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  349. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  350. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  351. package/templates/apps/api/src/services/person.ts +0 -1211
  352. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  353. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  354. package/templates/apps/api/src/services/project-listings.ts +0 -2085
  355. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  356. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  357. package/templates/apps/api/src/services/revalidation.ts +0 -235
  358. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  359. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  360. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  361. package/templates/apps/api/src/services/verified-email.ts +0 -71
  362. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  363. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  364. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  365. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  366. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  367. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  368. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  369. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  370. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  371. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  372. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  373. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  374. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  375. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  376. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  377. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  378. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
  379. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  380. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  381. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  382. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
  383. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
  384. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  385. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  386. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
  387. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  388. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
  389. package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
  390. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  391. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  392. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
  393. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
  394. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
  395. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
  396. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  397. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  398. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
  399. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
  400. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  401. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  402. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  403. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  404. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  405. package/templates/apps/mcp/src/abuse/index.ts +0 -349
  406. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  407. package/templates/apps/mcp/src/boot.ts +0 -167
  408. package/templates/apps/mcp/src/config/modules.ts +0 -43
  409. package/templates/apps/mcp/src/http-transport.ts +0 -564
  410. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  411. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  412. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
  413. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
  414. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  415. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  416. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  417. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  418. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  419. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  420. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  421. package/templates/apps/mcp/src/lib/site-config.ts +0 -53
  422. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  423. package/templates/apps/mcp/src/request-context.ts +0 -52
  424. package/templates/apps/mcp/src/server.ts +0 -343
  425. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  426. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  427. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
  428. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  429. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  430. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  431. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  432. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  433. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  434. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  435. package/templates/apps/mcp/src/tools/index.ts +0 -138
  436. package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
  437. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  438. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  439. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  440. package/templates/apps/mcp/src/tools/read_content.ts +0 -81
  441. package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
  442. package/templates/apps/mcp/src/tools/search_content.ts +0 -63
  443. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
  444. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  445. package/templates/database/CHANGELOG.md +0 -981
  446. package/templates/database/LICENSE +0 -202
  447. package/templates/database/NOTICE +0 -2
  448. package/templates/database/README.md +0 -243
  449. package/templates/database/__tests__/agents.db.test.ts +0 -320
  450. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  451. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  452. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  453. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  454. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  455. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  456. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  457. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  458. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  459. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  460. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  461. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  462. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  463. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  464. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  465. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  466. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  467. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  468. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  469. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  470. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  471. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  472. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  473. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  474. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  475. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  476. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  477. package/templates/database/__tests__/person.db.test.ts +0 -586
  478. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  479. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  480. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  481. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  482. package/templates/database/__tests__/seed.test.ts +0 -272
  483. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  484. package/templates/database/__tests__/tenancy.test.ts +0 -141
  485. package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
  486. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  487. package/templates/database/_gitignore +0 -8
  488. package/templates/database/auth/.gitkeep +0 -0
  489. package/templates/database/auth/schema.prisma +0 -641
  490. package/templates/database/config/.gitkeep +0 -0
  491. package/templates/database/config/schema.prisma +0 -441
  492. package/templates/database/content/.gitkeep +0 -0
  493. package/templates/database/content/schema.prisma +0 -339
  494. package/templates/database/eslint.config.mjs +0 -69
  495. package/templates/database/flows/listing-diagnostic.ts +0 -182
  496. package/templates/database/flows/presets.ts +0 -315
  497. package/templates/database/inbox/.gitkeep +0 -0
  498. package/templates/database/inbox/README.md +0 -3
  499. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  500. package/templates/database/inbox/schema.prisma +0 -1447
  501. package/templates/database/jest.config.cjs +0 -36
  502. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  503. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  504. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  505. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  506. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  507. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  508. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  509. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  510. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  511. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  512. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  513. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  514. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  515. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  516. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  517. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  518. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  519. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  520. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  521. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  522. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  523. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  524. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  525. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  526. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  527. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  528. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  529. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  530. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  531. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  532. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  533. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  534. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  535. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  536. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  537. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  538. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  539. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  540. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  541. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  542. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  543. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  544. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  545. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  546. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  547. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  548. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  549. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  550. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  551. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  552. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  553. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  554. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  555. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  556. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  557. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  558. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  559. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  560. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  561. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  562. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  563. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  564. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  565. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  566. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  567. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  568. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  569. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  570. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  571. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  572. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  573. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  574. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  575. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  576. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  577. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  578. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  579. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  580. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  581. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  582. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  583. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  584. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  585. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  586. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
  587. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
  588. package/templates/database/migrations/migration_lock.toml +0 -3
  589. package/templates/database/ops/.gitkeep +0 -0
  590. package/templates/database/ops/schema.prisma +0 -43
  591. package/templates/database/package.json +0 -89
  592. package/templates/database/schema.prisma +0 -71
  593. package/templates/database/scripts/db-generate-locked.sh +0 -80
  594. package/templates/database/scripts/person-backfill.sql +0 -132
  595. package/templates/database/tenancy.ts +0 -183
  596. package/templates/database/tsconfig.build.json +0 -16
  597. package/templates/database/tsconfig.flows.json +0 -15
  598. package/templates/database/tsconfig.json +0 -16
  599. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,1067 +0,0 @@
1
- -- #6254 — the tenancy fence keystone. THE ONE schema migration M32 emits
2
- -- (AC-1): #6256 #6257 #6258 #6259 #6260 #6171 #6314 land their model
3
- -- changes INTO this file rather than each adding their own (issue #6254
4
- -- comment, Sam, 2026-09-20; spec docs/specs/6254-the-composite-fk-pattern-
5
- -- workspacesiteidentity-spec.md D4).
6
- --
7
- -- ── What this file does ─────────────────────────────────────────────────────
8
- -- 1. `WorkspaceSiteIdentity` gains `@@unique([id, workspaceId])` (AC-2) — a
9
- -- named Postgres unique constraint, redundant today (`id` is already the
10
- -- PK) but the target a future composite FK can bind to (spec D3).
11
- -- 2. Four ALREADY-KEYED-but-nullable models — `leads`, `project_listings`,
12
- -- `flow_definitions`, `flow_sessions` — are retrofitted to NOT NULL
13
- -- `workspace_id` plus a `CHECK (workspace_id = site_id)` (spec D1b).
14
- -- 3. #6374 — the fence fan-out. ELEVEN `site_id` tables gain `workspace_id`
15
- -- and take the same five steps in the same order: `subscribers`,
16
- -- `callbacks`, `consent_grants`, `appointments`, `deliverable_assets`,
17
- -- `flow_checkouts`, `waitlist_experiments`, `waitlist_scores`,
18
- -- `waitlist_followup_sends`, `playbook_instances`, `site_listing_configs`.
19
- -- 4. #6374 — `listing_csv_sends` has NO `site_id`, so it gains `workspace_id`
20
- -- backfilled from its parent `project_listings` row, NOT NULL and the
21
- -- format CHECK, and NO equality CHECK (spec D2, operator 2026-09-20).
22
- -- 5. #6374 — `site_settings` has NO `site_id` either. It gains `workspace_id`
23
- -- backfilled to the sentinel workspace — but ONLY after an abort-not-guess
24
- -- guard (AC-14) refuses any unattributed row whose `key` already embeds a
25
- -- NON-sentinel workspace id, which the sentinel assignment would misfile —
26
- -- NOT NULL and the format CHECK, and
27
- -- its UNIQUE INDEX moves from `(key)` to `(workspace_id, key)` so two
28
- -- sellers may each hold a `feature_flags` row (spec D3, operator
29
- -- 2026-09-20). The `key` TEXT is left verbatim.
30
- -- 6. #6374 — `deliverable_grants` gains NO column and NO row is touched: its
31
- -- `workspace_id` stays nullable (NULL is the serve path's fail-closed
32
- -- sentinel). It gains only the NULL-TOLERANT
33
- -- `CHECK (workspace_id IS NULL OR workspace_id = site_id)`, so a STAMPED
34
- -- grant can never disagree with its site while a legacy one stays
35
- -- storable (spec D4).
36
- --
37
- -- ── Why a CHECK and not a foreign key (spec §0, §1.3, §1.4) ────────────────
38
- -- The issue's literal ask — a composite FK from these four models to
39
- -- `WorkspaceSiteIdentity` — is not buildable against this tree: (F1) the
40
- -- relation crosses the inbox -> config namespace boundary ADR-0009
41
- -- invariant #1 forbids, and `tools/lint-architecture.ts`'s
42
- -- cross-namespace-prisma-relation rule lints it; (F2) `site_id` never holds
43
- -- a `WorkspaceSiteIdentity.id` (`siteid_<uuidv7>`) — it holds the resolved
44
- -- WORKSPACE id (`wksp_<uuidv7>`) or the legacy alias `"default"`, so
45
- -- `references: [id, workspaceId]` would reject 100% of existing rows and
46
- -- 100% of writes from every current path; (F3) `WorkspaceSiteIdentity` is a
47
- -- per-workspace config blob written lazily by an upsert, not a site
48
- -- registry — a required FK parent would make anonymous lead capture (a
49
- -- never-gate surface) depend on a config row that need not exist.
50
- --
51
- -- AC-4's stated GOAL — "Postgres rejects a row whose workspaceId disagrees
52
- -- with its site's workspace" — is delivered without a FK: since `site_id`
53
- -- IS the workspace id (F2), the disagreement is exactly
54
- -- `workspace_id <> site_id`, and an equality CHECK makes it unstorable. This
55
- -- is strictly TIGHTER than the FK the issue asked for (the FK would admit
56
- -- any pair present in `workspace_site_identities`; the CHECK admits exactly
57
- -- one value per row). Disposed by the operator 2026-09-20, spec §10.1 Q1 ->
58
- -- Option A' (docs/internal/decisions/tenancy-fence-model-classification.md
59
- -- D1 amendment, same commit).
60
- --
61
- -- `DeliverableGrant` (the fifth already-keyed model) is DELIBERATELY NOT
62
- -- touched here. Its `workspace_id` has no source column to derive from (its
63
- -- own migration, 20260921120000_deliverable_grant_workspace_key, says so
64
- -- explicitly) and NULL is the serve path's fail-closed sentinel
65
- -- (`deliverable-asset-store.ts` `readDeliverableAsset`) — retrofitting it
66
- -- here would hand a legacy grant an authorization scope no mint granted, on
67
- -- a bearer-token download path. Carved out of this unit (spec §10.1 Q2 ->
68
- -- carve out); #6257 fences it at mint grain, where the workspace is known.
69
- --
70
- -- ── The CHECK requires `site_id` normalized FIRST (spec D1a, SEC-01) ───────
71
- -- A bare `CHECK (workspace_id = site_id)` is unsatisfiable for the shipped
72
- -- default: `site_id = 'default'` is the canonical seeded row, and both
73
- -- precedent backfills (20260919140000, 20260919120000) already resolve
74
- -- `'default'` to the sentinel workspace id in `workspace_id` while leaving
75
- -- `site_id` untouched. So this migration rewrites `site_id = 'default'` to
76
- -- the same sentinel on every fenced table BEFORE the CHECK lands — the
77
- -- application-code half of D1a (the write paths that still stamp the raw
78
- -- alias) is fixed in the same PR (apps/api/src/lib/site.ts,
79
- -- routes/flows/index.ts, routes/flows/recommender.ts, bin/seed-presets.ts,
80
- -- services/listing-config.ts, services/flow-engine.ts,
81
- -- services/project-listings.ts, services/recommender-capture.ts).
82
- --
83
- -- ── Ordering (spec D2) — never guess, never delete, abort if unresolvable ──
84
- -- 1. Normalize site_id ('default' -> sentinel).
85
- -- 2. Resolve any remaining NULL workspace_id via an INNER JOIN to
86
- -- "workspaces" only (never an assignment) — a row whose site_id resolves
87
- -- to nothing is left NULL, never guessed. This is mostly a no-op today:
88
- -- 20260919140000/20260919120000 already backfilled these four tables:
89
- -- kept here so this migration proves its OWN correctness re-run against
90
- -- fixture rows (NFR3, § 8.3) rather than depending on migration order.
91
- -- 3. Post-assert: nothing lost, nothing guessed, nothing missed — carried
92
- -- forward from the precedent — PLUS a NEW assertion this migration adds
93
- -- and the precedent could not: zero rows remain NULL. Unlike the
94
- -- precedent (which only RAISE NOTICEs on a remaining NULL, because it
95
- -- never enforces NOT NULL), THIS migration is about to enforce NOT NULL,
96
- -- so an unresolvable row must abort the transaction rather than pass
97
- -- through silently — "the same silent data loss AC-7 exists to close"
98
- -- (spec §0), never a delete, never a guess.
99
- -- 4. THEN `SET NOT NULL`, then the CHECKs.
100
- --
101
- -- ── Re-runnability (NFR3) ───────────────────────────────────────────────────
102
- -- `ADD COLUMN`/`CREATE INDEX` are not needed here (the columns/indexes
103
- -- already exist from the precedent migrations). The equality CHECKs use
104
- -- `DROP CONSTRAINT IF EXISTS` then `ADD CONSTRAINT` — NOT the precedent's
105
- -- existence-guarded `DO $$` block, which would silently no-op the
106
- -- TIGHTENING this migration performs on any database that already carries a
107
- -- same-named constraint (spec D5, SEC-06: a false-green control). The
108
- -- `WorkspaceSiteIdentity` composite unique is a pure ADD (nothing to
109
- -- tighten), so it keeps the existence-guarded idiom.
110
- --
111
- -- ── Rollback (NFR5) ─────────────────────────────────────────────────────────
112
- -- Reverses the CONSTRAINTS only, not the tenant attribution:
113
- -- ALTER TABLE "leads" DROP CONSTRAINT "leads_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
114
- -- ALTER TABLE "project_listings" DROP CONSTRAINT "project_listings_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
115
- -- ALTER TABLE "flow_definitions" DROP CONSTRAINT "flow_definitions_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
116
- -- ALTER TABLE "flow_sessions" DROP CONSTRAINT "flow_sessions_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
117
- -- ALTER TABLE "workspace_site_identities" DROP CONSTRAINT "workspace_site_identities_id_workspace_id_key";
118
- -- #6374's own reversal, same posture — CONSTRAINTS only, never the tenant
119
- -- attribution: for each of the twelve tables it fences
120
- -- ALTER TABLE "<table>" DROP CONSTRAINT "<table>_workspace_site_agree",
121
- -- DROP CONSTRAINT "<table>_workspace_id_format",
122
- -- ALTER COLUMN "workspace_id" DROP NOT NULL;
123
- -- (`listing_csv_sends` and `site_settings` have no `_workspace_site_agree`;
124
- -- `site_settings` additionally needs its old `site_settings_key_key` unique
125
- -- index recreated, which is only possible while no two workspaces hold the
126
- -- same key.) `deliverable_grants` reverses by dropping its one new CHECK.
127
- -- Unlike the four keystone tables, `workspace_id` does NOT predate this
128
- -- migration on the twelve, so a full DOWN would also DROP COLUMN it — which
129
- -- DOES discard tenant attribution. Stated so it is not read as a safe revert.
130
- -- `workspace_id` predates this migration on all four tables (§1.6), and the
131
- -- `site_id` normalization (D1a) is one-way — a DOWN does not un-normalize
132
- -- `site_id`, because the pre-normalization alias value is not recoverable
133
- -- from the row. NOT a full revert; stated so it is not read as one.
134
-
135
- -- The census the post-assertions compare against. ON COMMIT DROP: `prisma
136
- -- migrate` runs each migration in one transaction, so this disappears with it.
137
- CREATE TEMP TABLE "_6254_fence_census" ON COMMIT DROP AS
138
- SELECT (SELECT count(*) FROM "leads") AS l,
139
- (SELECT count(*) FROM "project_listings") AS p,
140
- (SELECT count(*) FROM "flow_definitions") AS d,
141
- (SELECT count(*) FROM "flow_sessions") AS s;
142
-
143
- -- ── AC-2 — WorkspaceSiteIdentity composite unique ──────────────────────────
144
- DO $$ BEGIN
145
- IF NOT EXISTS (
146
- SELECT 1 FROM pg_constraint WHERE conname = 'workspace_site_identities_id_workspace_id_key'
147
- ) THEN
148
- ALTER TABLE "workspace_site_identities"
149
- ADD CONSTRAINT "workspace_site_identities_id_workspace_id_key" UNIQUE ("id", "workspace_id");
150
- END IF;
151
- END $$;
152
-
153
- -- ── Step 1 (D1a) — normalize site_id: 'default' -> the sentinel workspace id ─
154
- -- The sentinel id is a literal because SQL cannot import database/tenancy.ts
155
- -- (spec §5.3 step 5); database/__tests__/composite-fence.db.test.ts pins the
156
- -- same literal against the exported DEFAULT_WORKSPACE_ID constant, mirroring
157
- -- the #6155/#6203 precedents' own technique.
158
- UPDATE "leads"
159
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
160
- WHERE "site_id" = 'default';
161
-
162
- UPDATE "project_listings"
163
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
164
- WHERE "site_id" = 'default';
165
-
166
- UPDATE "flow_definitions"
167
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
168
- WHERE "site_id" = 'default';
169
-
170
- UPDATE "flow_sessions"
171
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
172
- WHERE "site_id" = 'default';
173
-
174
- -- ── Step 2 (D2) — resolve any remaining NULL workspace_id via INNER JOIN ────
175
- -- site_id is already normalized (step 1), so no CASE/alias branch is needed
176
- -- here, unlike the precedent migrations this runs after. A row whose site_id
177
- -- resolves to nothing is simply not touched and stays NULL — never guessed.
178
- UPDATE "leads" l
179
- SET "workspace_id" = w."id"
180
- FROM "workspaces" w
181
- WHERE l."workspace_id" IS NULL
182
- AND w."id" = l."site_id";
183
-
184
- UPDATE "project_listings" p
185
- SET "workspace_id" = w."id"
186
- FROM "workspaces" w
187
- WHERE p."workspace_id" IS NULL
188
- AND w."id" = p."site_id";
189
-
190
- UPDATE "flow_definitions" d
191
- SET "workspace_id" = w."id"
192
- FROM "workspaces" w
193
- WHERE d."workspace_id" IS NULL
194
- AND w."id" = d."site_id";
195
-
196
- UPDATE "flow_sessions" s
197
- SET "workspace_id" = w."id"
198
- FROM "workspaces" w
199
- WHERE s."workspace_id" IS NULL
200
- AND w."id" = s."site_id";
201
-
202
- -- ── Step 3 — POST-ASSERTIONS ─────────────────────────────────────────────────
203
- DO $$
204
- DECLARE
205
- before_l bigint; before_p bigint; before_d bigint; before_s bigint;
206
- after_l bigint; after_p bigint; after_d bigint; after_s bigint;
207
- bad bigint;
208
- BEGIN
209
- SELECT l, p, d, s INTO before_l, before_p, before_d, before_s FROM "_6254_fence_census";
210
- SELECT count(*) INTO after_l FROM "leads";
211
- SELECT count(*) INTO after_p FROM "project_listings";
212
- SELECT count(*) INTO after_d FROM "flow_definitions";
213
- SELECT count(*) INTO after_s FROM "flow_sessions";
214
-
215
- -- 1. Nothing lost: row counts unchanged.
216
- IF before_l <> after_l OR before_p <> after_p OR before_d <> after_d OR before_s <> after_s THEN
217
- RAISE EXCEPTION
218
- '#6254 post-assertion: row count changed across a purely additive migration (leads % -> %, project_listings % -> %, flow_definitions % -> %, flow_sessions % -> %)',
219
- before_l, after_l, before_p, after_p, before_d, after_d, before_s, after_s;
220
- END IF;
221
-
222
- -- 2. Nothing guessed: no row's workspace_id names a workspace absent from "workspaces".
223
- SELECT count(*) INTO bad FROM "leads" l
224
- WHERE l."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = l."workspace_id");
225
- IF bad <> 0 THEN
226
- RAISE EXCEPTION '#6254 post-assertion: % leads row(s) carry a workspace_id absent from workspaces', bad;
227
- END IF;
228
- SELECT count(*) INTO bad FROM "project_listings" p
229
- WHERE p."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = p."workspace_id");
230
- IF bad <> 0 THEN
231
- RAISE EXCEPTION '#6254 post-assertion: % project_listings row(s) carry a workspace_id absent from workspaces', bad;
232
- END IF;
233
- SELECT count(*) INTO bad FROM "flow_definitions" d
234
- WHERE d."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = d."workspace_id");
235
- IF bad <> 0 THEN
236
- RAISE EXCEPTION '#6254 post-assertion: % flow_definitions row(s) carry a workspace_id absent from workspaces', bad;
237
- END IF;
238
- SELECT count(*) INTO bad FROM "flow_sessions" s
239
- WHERE s."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = s."workspace_id");
240
- IF bad <> 0 THEN
241
- RAISE EXCEPTION '#6254 post-assertion: % flow_sessions row(s) carry a workspace_id absent from workspaces', bad;
242
- END IF;
243
-
244
- -- 3. Nothing missed: no row whose (normalized) site_id names a real workspace
245
- -- came out of the backfill with workspace_id still NULL.
246
- SELECT count(*) INTO bad FROM "leads" l
247
- WHERE l."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = l."site_id");
248
- IF bad <> 0 THEN
249
- RAISE EXCEPTION '#6254 post-assertion: % leads row(s) had a resolvable site_id but came out of the backfill NULL', bad;
250
- END IF;
251
- SELECT count(*) INTO bad FROM "project_listings" p
252
- WHERE p."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = p."site_id");
253
- IF bad <> 0 THEN
254
- RAISE EXCEPTION '#6254 post-assertion: % project_listings row(s) had a resolvable site_id but came out of the backfill NULL', bad;
255
- END IF;
256
- SELECT count(*) INTO bad FROM "flow_definitions" d
257
- WHERE d."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = d."site_id");
258
- IF bad <> 0 THEN
259
- RAISE EXCEPTION '#6254 post-assertion: % flow_definitions row(s) had a resolvable site_id but came out of the backfill NULL', bad;
260
- END IF;
261
- SELECT count(*) INTO bad FROM "flow_sessions" s
262
- WHERE s."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = s."site_id");
263
- IF bad <> 0 THEN
264
- RAISE EXCEPTION '#6254 post-assertion: % flow_sessions row(s) had a resolvable site_id but came out of the backfill NULL', bad;
265
- END IF;
266
-
267
- -- 4. AC-7 / spec D2 point 4 — an unresolvable row must ABORT, never pass
268
- -- through as a silent NULL: this migration is about to SET NOT NULL, so
269
- -- (unlike the precedent's NOTICE-only posture) a remaining NULL here
270
- -- means a row's site_id names no workspace this database holds, and
271
- -- there is no correct value to write. Never deleted, never guessed.
272
- SELECT count(*) INTO bad FROM "leads" WHERE "workspace_id" IS NULL;
273
- IF bad <> 0 THEN
274
- RAISE EXCEPTION '#6254: % leads row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
275
- END IF;
276
- SELECT count(*) INTO bad FROM "project_listings" WHERE "workspace_id" IS NULL;
277
- IF bad <> 0 THEN
278
- RAISE EXCEPTION '#6254: % project_listings row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
279
- END IF;
280
- SELECT count(*) INTO bad FROM "flow_definitions" WHERE "workspace_id" IS NULL;
281
- IF bad <> 0 THEN
282
- RAISE EXCEPTION '#6254: % flow_definitions row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
283
- END IF;
284
- SELECT count(*) INTO bad FROM "flow_sessions" WHERE "workspace_id" IS NULL;
285
- IF bad <> 0 THEN
286
- RAISE EXCEPTION '#6254: % flow_sessions row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
287
- END IF;
288
- END $$;
289
-
290
- -- ── Step 4 — SET NOT NULL (AC-3, AC-4, AC-7) ────────────────────────────────
291
- ALTER TABLE "leads" ALTER COLUMN "workspace_id" SET NOT NULL;
292
- ALTER TABLE "project_listings" ALTER COLUMN "workspace_id" SET NOT NULL;
293
- ALTER TABLE "flow_definitions" ALTER COLUMN "workspace_id" SET NOT NULL;
294
- ALTER TABLE "flow_sessions" ALTER COLUMN "workspace_id" SET NOT NULL;
295
-
296
- -- ── Step 5 — equality CHECKs (AC-3, AC-4, AC-5) — DROP then ADD, not guarded ─
297
- ALTER TABLE "leads" DROP CONSTRAINT IF EXISTS "leads_workspace_site_agree";
298
- ALTER TABLE "leads" ADD CONSTRAINT "leads_workspace_site_agree" CHECK ("workspace_id" = "site_id");
299
-
300
- ALTER TABLE "project_listings" DROP CONSTRAINT IF EXISTS "project_listings_workspace_site_agree";
301
- ALTER TABLE "project_listings" ADD CONSTRAINT "project_listings_workspace_site_agree" CHECK ("workspace_id" = "site_id");
302
-
303
- ALTER TABLE "flow_definitions" DROP CONSTRAINT IF EXISTS "flow_definitions_workspace_site_agree";
304
- ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_workspace_site_agree" CHECK ("workspace_id" = "site_id");
305
-
306
- ALTER TABLE "flow_sessions" DROP CONSTRAINT IF EXISTS "flow_sessions_workspace_site_agree";
307
- ALTER TABLE "flow_sessions" ADD CONSTRAINT "flow_sessions_workspace_site_agree" CHECK ("workspace_id" = "site_id");
308
-
309
- -- ════════════════════════════════════════════════════════════════════════════
310
- -- #6374 — the fence fan-out. THIRTEEN more models join the fence, amended INTO
311
- -- this file rather than added as a new migration, because #6254 AC-1 makes
312
- -- this THE ONE schema migration M32 emits. Amending a merged migration is a
313
- -- silent no-op on a database that already applied it; the operator disposed
314
- -- the assumption that makes that safe on 2026-09-20 (spec § 1.5 — no
315
- -- persistent database carries state that must survive; every site is
316
- -- rebuildable). If that assumption ever stops holding, everything below moves
317
- -- verbatim into a new migration directory and nothing else changes.
318
- --
319
- -- Eleven tables carry `site_id` and take the keystone shape above unchanged,
320
- -- in the keystone's own five-step order: normalize -> backfill by INNER JOIN
321
- -- -> four post-assertions -> SET NOT NULL -> the two CHECKs. Two do not carry
322
- -- `site_id` and take the operator-disposed shapes (spec § 5 D2/D3), and
323
- -- `deliverable_grants` gains a NULL-TOLERANT equality CHECK only (D4).
324
- -- ════════════════════════════════════════════════════════════════════════════
325
-
326
- -- The #6374 census, the twin of the #6254 one above. ON COMMIT DROP for the
327
- -- same reason: `prisma migrate` runs each migration in one transaction.
328
- CREATE TEMP TABLE "_6374_fence_census" ON COMMIT DROP AS
329
- SELECT (SELECT count(*) FROM "subscribers") AS "subscribers"
330
- , (SELECT count(*) FROM "callbacks") AS "callbacks"
331
- , (SELECT count(*) FROM "consent_grants") AS "consent_grants"
332
- , (SELECT count(*) FROM "appointments") AS "appointments"
333
- , (SELECT count(*) FROM "deliverable_assets") AS "deliverable_assets"
334
- , (SELECT count(*) FROM "flow_checkouts") AS "flow_checkouts"
335
- , (SELECT count(*) FROM "waitlist_experiments") AS "waitlist_experiments"
336
- , (SELECT count(*) FROM "waitlist_scores") AS "waitlist_scores"
337
- , (SELECT count(*) FROM "waitlist_followup_sends") AS "waitlist_followup_sends"
338
- , (SELECT count(*) FROM "playbook_instances") AS "playbook_instances"
339
- , (SELECT count(*) FROM "site_listing_configs") AS "site_listing_configs"
340
- ;
341
-
342
- -- ── Step 1 (D1 step 1) — the column and its index ─────────────────────────
343
- -- Unlike the four keystone tables, these eleven have no `workspace_id`
344
- -- column yet, so it is ADDED nullable here and tightened in step 4. ONE
345
- -- index per table and nothing query-shaped (D6): a read-side unit that turns
346
- -- out to need a composite gets a post-M32 migration, filed with the query
347
- -- that justifies it.
348
- ALTER TABLE "subscribers" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
349
- CREATE INDEX IF NOT EXISTS "subscribers_workspace_id_idx" ON "subscribers" ("workspace_id");
350
- ALTER TABLE "callbacks" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
351
- CREATE INDEX IF NOT EXISTS "callbacks_workspace_id_idx" ON "callbacks" ("workspace_id");
352
- ALTER TABLE "consent_grants" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
353
- CREATE INDEX IF NOT EXISTS "consent_grants_workspace_id_idx" ON "consent_grants" ("workspace_id");
354
- ALTER TABLE "appointments" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
355
- CREATE INDEX IF NOT EXISTS "appointments_workspace_id_idx" ON "appointments" ("workspace_id");
356
- ALTER TABLE "deliverable_assets" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
357
- CREATE INDEX IF NOT EXISTS "deliverable_assets_workspace_id_idx" ON "deliverable_assets" ("workspace_id");
358
- ALTER TABLE "flow_checkouts" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
359
- CREATE INDEX IF NOT EXISTS "flow_checkouts_workspace_id_idx" ON "flow_checkouts" ("workspace_id");
360
- ALTER TABLE "waitlist_experiments" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
361
- CREATE INDEX IF NOT EXISTS "waitlist_experiments_workspace_id_idx" ON "waitlist_experiments" ("workspace_id");
362
- ALTER TABLE "waitlist_scores" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
363
- CREATE INDEX IF NOT EXISTS "waitlist_scores_workspace_id_idx" ON "waitlist_scores" ("workspace_id");
364
- ALTER TABLE "waitlist_followup_sends" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
365
- CREATE INDEX IF NOT EXISTS "waitlist_followup_sends_workspace_id_idx" ON "waitlist_followup_sends" ("workspace_id");
366
- ALTER TABLE "playbook_instances" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
367
- CREATE INDEX IF NOT EXISTS "playbook_instances_workspace_id_idx" ON "playbook_instances" ("workspace_id");
368
- ALTER TABLE "site_listing_configs" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
369
- CREATE INDEX IF NOT EXISTS "site_listing_configs_workspace_id_idx" ON "site_listing_configs" ("workspace_id");
370
-
371
- -- ── Step 2 (D1 step 2 / keystone D1a) — normalize site_id -> the sentinel ──
372
- -- The same literal, and the same one-way rewrite, the four keystone tables
373
- -- above already took. database/__tests__/composite-fence.db.test.ts pins
374
- -- EVERY `wksp_` literal in this file against the exported
375
- -- DEFAULT_WORKSPACE_ID, so these eleven cannot drift from it either.
376
- UPDATE "subscribers"
377
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
378
- WHERE "site_id" = 'default';
379
-
380
- UPDATE "callbacks"
381
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
382
- WHERE "site_id" = 'default';
383
-
384
- UPDATE "consent_grants"
385
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
386
- WHERE "site_id" = 'default';
387
-
388
- UPDATE "appointments"
389
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
390
- WHERE "site_id" = 'default';
391
-
392
- UPDATE "deliverable_assets"
393
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
394
- WHERE "site_id" = 'default';
395
-
396
- UPDATE "flow_checkouts"
397
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
398
- WHERE "site_id" = 'default';
399
-
400
- UPDATE "waitlist_experiments"
401
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
402
- WHERE "site_id" = 'default';
403
-
404
- UPDATE "waitlist_scores"
405
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
406
- WHERE "site_id" = 'default';
407
-
408
- UPDATE "waitlist_followup_sends"
409
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
410
- WHERE "site_id" = 'default';
411
-
412
- UPDATE "playbook_instances"
413
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
414
- WHERE "site_id" = 'default';
415
-
416
- UPDATE "site_listing_configs"
417
- SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
418
- WHERE "site_id" = 'default';
419
-
420
- -- ── Step 3 (D1 step 3 / keystone D2) — backfill by INNER JOIN, never by assignment ─
421
- -- site_id is already normalized (step 1), so no CASE/alias branch is needed.
422
- -- A row whose site_id resolves to nothing is simply not touched and stays
423
- -- NULL — never guessed. Step 3's assertion 4 then aborts on it.
424
- UPDATE "subscribers" x
425
- SET "workspace_id" = w."id"
426
- FROM "workspaces" w
427
- WHERE x."workspace_id" IS NULL
428
- AND w."id" = x."site_id";
429
-
430
- UPDATE "callbacks" x
431
- SET "workspace_id" = w."id"
432
- FROM "workspaces" w
433
- WHERE x."workspace_id" IS NULL
434
- AND w."id" = x."site_id";
435
-
436
- UPDATE "consent_grants" x
437
- SET "workspace_id" = w."id"
438
- FROM "workspaces" w
439
- WHERE x."workspace_id" IS NULL
440
- AND w."id" = x."site_id";
441
-
442
- UPDATE "appointments" x
443
- SET "workspace_id" = w."id"
444
- FROM "workspaces" w
445
- WHERE x."workspace_id" IS NULL
446
- AND w."id" = x."site_id";
447
-
448
- UPDATE "deliverable_assets" x
449
- SET "workspace_id" = w."id"
450
- FROM "workspaces" w
451
- WHERE x."workspace_id" IS NULL
452
- AND w."id" = x."site_id";
453
-
454
- UPDATE "flow_checkouts" x
455
- SET "workspace_id" = w."id"
456
- FROM "workspaces" w
457
- WHERE x."workspace_id" IS NULL
458
- AND w."id" = x."site_id";
459
-
460
- UPDATE "waitlist_experiments" x
461
- SET "workspace_id" = w."id"
462
- FROM "workspaces" w
463
- WHERE x."workspace_id" IS NULL
464
- AND w."id" = x."site_id";
465
-
466
- UPDATE "waitlist_scores" x
467
- SET "workspace_id" = w."id"
468
- FROM "workspaces" w
469
- WHERE x."workspace_id" IS NULL
470
- AND w."id" = x."site_id";
471
-
472
- UPDATE "waitlist_followup_sends" x
473
- SET "workspace_id" = w."id"
474
- FROM "workspaces" w
475
- WHERE x."workspace_id" IS NULL
476
- AND w."id" = x."site_id";
477
-
478
- UPDATE "playbook_instances" x
479
- SET "workspace_id" = w."id"
480
- FROM "workspaces" w
481
- WHERE x."workspace_id" IS NULL
482
- AND w."id" = x."site_id";
483
-
484
- UPDATE "site_listing_configs" x
485
- SET "workspace_id" = w."id"
486
- FROM "workspaces" w
487
- WHERE x."workspace_id" IS NULL
488
- AND w."id" = x."site_id";
489
-
490
- -- ── Step 4 (D1 step 4) — POST-ASSERTIONS, four per table ──────────────────
491
- -- Nothing lost (row census unchanged), nothing guessed (no workspace_id
492
- -- absent from "workspaces"), nothing missed (no resolvable site_id left
493
- -- NULL), and ABORT on any remaining NULL — this migration is about to
494
- -- SET NOT NULL, so an unresolvable row has no correct value to write. Never
495
- -- deleted, never guessed (NFR3).
496
- DO $$
497
- DECLARE
498
- before_n bigint; after_n bigint; bad bigint;
499
- BEGIN
500
- -- 1. Nothing lost — "subscribers"
501
- SELECT "subscribers" INTO before_n FROM "_6374_fence_census";
502
- SELECT count(*) INTO after_n FROM "subscribers";
503
- IF before_n <> after_n THEN
504
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (subscribers % -> %)', before_n, after_n;
505
- END IF;
506
-
507
- -- 2. Nothing guessed — "subscribers"
508
- SELECT count(*) INTO bad FROM "subscribers" x
509
- WHERE x."workspace_id" IS NOT NULL
510
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
511
- IF bad <> 0 THEN
512
- RAISE EXCEPTION '#6374 post-assertion: % subscribers row(s) carry a workspace_id absent from workspaces', bad;
513
- END IF;
514
-
515
- -- 3. Nothing missed — "subscribers"
516
- SELECT count(*) INTO bad FROM "subscribers" x
517
- WHERE x."workspace_id" IS NULL
518
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
519
- IF bad <> 0 THEN
520
- RAISE EXCEPTION '#6374 post-assertion: % subscribers row(s) had a resolvable site_id but came out of the backfill NULL', bad;
521
- END IF;
522
-
523
- -- 4. Abort on any remaining NULL — "subscribers"
524
- SELECT count(*) INTO bad FROM "subscribers" WHERE "workspace_id" IS NULL;
525
- IF bad <> 0 THEN
526
- RAISE EXCEPTION '#6374: % subscribers row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
527
- END IF;
528
-
529
- -- 1. Nothing lost — "callbacks"
530
- SELECT "callbacks" INTO before_n FROM "_6374_fence_census";
531
- SELECT count(*) INTO after_n FROM "callbacks";
532
- IF before_n <> after_n THEN
533
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (callbacks % -> %)', before_n, after_n;
534
- END IF;
535
-
536
- -- 2. Nothing guessed — "callbacks"
537
- SELECT count(*) INTO bad FROM "callbacks" x
538
- WHERE x."workspace_id" IS NOT NULL
539
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
540
- IF bad <> 0 THEN
541
- RAISE EXCEPTION '#6374 post-assertion: % callbacks row(s) carry a workspace_id absent from workspaces', bad;
542
- END IF;
543
-
544
- -- 3. Nothing missed — "callbacks"
545
- SELECT count(*) INTO bad FROM "callbacks" x
546
- WHERE x."workspace_id" IS NULL
547
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
548
- IF bad <> 0 THEN
549
- RAISE EXCEPTION '#6374 post-assertion: % callbacks row(s) had a resolvable site_id but came out of the backfill NULL', bad;
550
- END IF;
551
-
552
- -- 4. Abort on any remaining NULL — "callbacks"
553
- SELECT count(*) INTO bad FROM "callbacks" WHERE "workspace_id" IS NULL;
554
- IF bad <> 0 THEN
555
- RAISE EXCEPTION '#6374: % callbacks row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
556
- END IF;
557
-
558
- -- 1. Nothing lost — "consent_grants"
559
- SELECT "consent_grants" INTO before_n FROM "_6374_fence_census";
560
- SELECT count(*) INTO after_n FROM "consent_grants";
561
- IF before_n <> after_n THEN
562
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (consent_grants % -> %)', before_n, after_n;
563
- END IF;
564
-
565
- -- 2. Nothing guessed — "consent_grants"
566
- SELECT count(*) INTO bad FROM "consent_grants" x
567
- WHERE x."workspace_id" IS NOT NULL
568
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
569
- IF bad <> 0 THEN
570
- RAISE EXCEPTION '#6374 post-assertion: % consent_grants row(s) carry a workspace_id absent from workspaces', bad;
571
- END IF;
572
-
573
- -- 3. Nothing missed — "consent_grants"
574
- SELECT count(*) INTO bad FROM "consent_grants" x
575
- WHERE x."workspace_id" IS NULL
576
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
577
- IF bad <> 0 THEN
578
- RAISE EXCEPTION '#6374 post-assertion: % consent_grants row(s) had a resolvable site_id but came out of the backfill NULL', bad;
579
- END IF;
580
-
581
- -- 4. Abort on any remaining NULL — "consent_grants"
582
- SELECT count(*) INTO bad FROM "consent_grants" WHERE "workspace_id" IS NULL;
583
- IF bad <> 0 THEN
584
- RAISE EXCEPTION '#6374: % consent_grants row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
585
- END IF;
586
-
587
- -- 1. Nothing lost — "appointments"
588
- SELECT "appointments" INTO before_n FROM "_6374_fence_census";
589
- SELECT count(*) INTO after_n FROM "appointments";
590
- IF before_n <> after_n THEN
591
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (appointments % -> %)', before_n, after_n;
592
- END IF;
593
-
594
- -- 2. Nothing guessed — "appointments"
595
- SELECT count(*) INTO bad FROM "appointments" x
596
- WHERE x."workspace_id" IS NOT NULL
597
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
598
- IF bad <> 0 THEN
599
- RAISE EXCEPTION '#6374 post-assertion: % appointments row(s) carry a workspace_id absent from workspaces', bad;
600
- END IF;
601
-
602
- -- 3. Nothing missed — "appointments"
603
- SELECT count(*) INTO bad FROM "appointments" x
604
- WHERE x."workspace_id" IS NULL
605
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
606
- IF bad <> 0 THEN
607
- RAISE EXCEPTION '#6374 post-assertion: % appointments row(s) had a resolvable site_id but came out of the backfill NULL', bad;
608
- END IF;
609
-
610
- -- 4. Abort on any remaining NULL — "appointments"
611
- SELECT count(*) INTO bad FROM "appointments" WHERE "workspace_id" IS NULL;
612
- IF bad <> 0 THEN
613
- RAISE EXCEPTION '#6374: % appointments row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
614
- END IF;
615
-
616
- -- 1. Nothing lost — "deliverable_assets"
617
- SELECT "deliverable_assets" INTO before_n FROM "_6374_fence_census";
618
- SELECT count(*) INTO after_n FROM "deliverable_assets";
619
- IF before_n <> after_n THEN
620
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (deliverable_assets % -> %)', before_n, after_n;
621
- END IF;
622
-
623
- -- 2. Nothing guessed — "deliverable_assets"
624
- SELECT count(*) INTO bad FROM "deliverable_assets" x
625
- WHERE x."workspace_id" IS NOT NULL
626
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
627
- IF bad <> 0 THEN
628
- RAISE EXCEPTION '#6374 post-assertion: % deliverable_assets row(s) carry a workspace_id absent from workspaces', bad;
629
- END IF;
630
-
631
- -- 3. Nothing missed — "deliverable_assets"
632
- SELECT count(*) INTO bad FROM "deliverable_assets" x
633
- WHERE x."workspace_id" IS NULL
634
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
635
- IF bad <> 0 THEN
636
- RAISE EXCEPTION '#6374 post-assertion: % deliverable_assets row(s) had a resolvable site_id but came out of the backfill NULL', bad;
637
- END IF;
638
-
639
- -- 4. Abort on any remaining NULL — "deliverable_assets"
640
- SELECT count(*) INTO bad FROM "deliverable_assets" WHERE "workspace_id" IS NULL;
641
- IF bad <> 0 THEN
642
- RAISE EXCEPTION '#6374: % deliverable_assets row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
643
- END IF;
644
-
645
- -- 1. Nothing lost — "flow_checkouts"
646
- SELECT "flow_checkouts" INTO before_n FROM "_6374_fence_census";
647
- SELECT count(*) INTO after_n FROM "flow_checkouts";
648
- IF before_n <> after_n THEN
649
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (flow_checkouts % -> %)', before_n, after_n;
650
- END IF;
651
-
652
- -- 2. Nothing guessed — "flow_checkouts"
653
- SELECT count(*) INTO bad FROM "flow_checkouts" x
654
- WHERE x."workspace_id" IS NOT NULL
655
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
656
- IF bad <> 0 THEN
657
- RAISE EXCEPTION '#6374 post-assertion: % flow_checkouts row(s) carry a workspace_id absent from workspaces', bad;
658
- END IF;
659
-
660
- -- 3. Nothing missed — "flow_checkouts"
661
- SELECT count(*) INTO bad FROM "flow_checkouts" x
662
- WHERE x."workspace_id" IS NULL
663
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
664
- IF bad <> 0 THEN
665
- RAISE EXCEPTION '#6374 post-assertion: % flow_checkouts row(s) had a resolvable site_id but came out of the backfill NULL', bad;
666
- END IF;
667
-
668
- -- 4. Abort on any remaining NULL — "flow_checkouts"
669
- SELECT count(*) INTO bad FROM "flow_checkouts" WHERE "workspace_id" IS NULL;
670
- IF bad <> 0 THEN
671
- RAISE EXCEPTION '#6374: % flow_checkouts row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
672
- END IF;
673
-
674
- -- 1. Nothing lost — "waitlist_experiments"
675
- SELECT "waitlist_experiments" INTO before_n FROM "_6374_fence_census";
676
- SELECT count(*) INTO after_n FROM "waitlist_experiments";
677
- IF before_n <> after_n THEN
678
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_experiments % -> %)', before_n, after_n;
679
- END IF;
680
-
681
- -- 2. Nothing guessed — "waitlist_experiments"
682
- SELECT count(*) INTO bad FROM "waitlist_experiments" x
683
- WHERE x."workspace_id" IS NOT NULL
684
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
685
- IF bad <> 0 THEN
686
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_experiments row(s) carry a workspace_id absent from workspaces', bad;
687
- END IF;
688
-
689
- -- 3. Nothing missed — "waitlist_experiments"
690
- SELECT count(*) INTO bad FROM "waitlist_experiments" x
691
- WHERE x."workspace_id" IS NULL
692
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
693
- IF bad <> 0 THEN
694
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_experiments row(s) had a resolvable site_id but came out of the backfill NULL', bad;
695
- END IF;
696
-
697
- -- 4. Abort on any remaining NULL — "waitlist_experiments"
698
- SELECT count(*) INTO bad FROM "waitlist_experiments" WHERE "workspace_id" IS NULL;
699
- IF bad <> 0 THEN
700
- RAISE EXCEPTION '#6374: % waitlist_experiments row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
701
- END IF;
702
-
703
- -- 1. Nothing lost — "waitlist_scores"
704
- SELECT "waitlist_scores" INTO before_n FROM "_6374_fence_census";
705
- SELECT count(*) INTO after_n FROM "waitlist_scores";
706
- IF before_n <> after_n THEN
707
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_scores % -> %)', before_n, after_n;
708
- END IF;
709
-
710
- -- 2. Nothing guessed — "waitlist_scores"
711
- SELECT count(*) INTO bad FROM "waitlist_scores" x
712
- WHERE x."workspace_id" IS NOT NULL
713
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
714
- IF bad <> 0 THEN
715
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_scores row(s) carry a workspace_id absent from workspaces', bad;
716
- END IF;
717
-
718
- -- 3. Nothing missed — "waitlist_scores"
719
- SELECT count(*) INTO bad FROM "waitlist_scores" x
720
- WHERE x."workspace_id" IS NULL
721
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
722
- IF bad <> 0 THEN
723
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_scores row(s) had a resolvable site_id but came out of the backfill NULL', bad;
724
- END IF;
725
-
726
- -- 4. Abort on any remaining NULL — "waitlist_scores"
727
- SELECT count(*) INTO bad FROM "waitlist_scores" WHERE "workspace_id" IS NULL;
728
- IF bad <> 0 THEN
729
- RAISE EXCEPTION '#6374: % waitlist_scores row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
730
- END IF;
731
-
732
- -- 1. Nothing lost — "waitlist_followup_sends"
733
- SELECT "waitlist_followup_sends" INTO before_n FROM "_6374_fence_census";
734
- SELECT count(*) INTO after_n FROM "waitlist_followup_sends";
735
- IF before_n <> after_n THEN
736
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_followup_sends % -> %)', before_n, after_n;
737
- END IF;
738
-
739
- -- 2. Nothing guessed — "waitlist_followup_sends"
740
- SELECT count(*) INTO bad FROM "waitlist_followup_sends" x
741
- WHERE x."workspace_id" IS NOT NULL
742
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
743
- IF bad <> 0 THEN
744
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_followup_sends row(s) carry a workspace_id absent from workspaces', bad;
745
- END IF;
746
-
747
- -- 3. Nothing missed — "waitlist_followup_sends"
748
- SELECT count(*) INTO bad FROM "waitlist_followup_sends" x
749
- WHERE x."workspace_id" IS NULL
750
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
751
- IF bad <> 0 THEN
752
- RAISE EXCEPTION '#6374 post-assertion: % waitlist_followup_sends row(s) had a resolvable site_id but came out of the backfill NULL', bad;
753
- END IF;
754
-
755
- -- 4. Abort on any remaining NULL — "waitlist_followup_sends"
756
- SELECT count(*) INTO bad FROM "waitlist_followup_sends" WHERE "workspace_id" IS NULL;
757
- IF bad <> 0 THEN
758
- RAISE EXCEPTION '#6374: % waitlist_followup_sends row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
759
- END IF;
760
-
761
- -- 1. Nothing lost — "playbook_instances"
762
- SELECT "playbook_instances" INTO before_n FROM "_6374_fence_census";
763
- SELECT count(*) INTO after_n FROM "playbook_instances";
764
- IF before_n <> after_n THEN
765
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (playbook_instances % -> %)', before_n, after_n;
766
- END IF;
767
-
768
- -- 2. Nothing guessed — "playbook_instances"
769
- SELECT count(*) INTO bad FROM "playbook_instances" x
770
- WHERE x."workspace_id" IS NOT NULL
771
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
772
- IF bad <> 0 THEN
773
- RAISE EXCEPTION '#6374 post-assertion: % playbook_instances row(s) carry a workspace_id absent from workspaces', bad;
774
- END IF;
775
-
776
- -- 3. Nothing missed — "playbook_instances"
777
- SELECT count(*) INTO bad FROM "playbook_instances" x
778
- WHERE x."workspace_id" IS NULL
779
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
780
- IF bad <> 0 THEN
781
- RAISE EXCEPTION '#6374 post-assertion: % playbook_instances row(s) had a resolvable site_id but came out of the backfill NULL', bad;
782
- END IF;
783
-
784
- -- 4. Abort on any remaining NULL — "playbook_instances"
785
- SELECT count(*) INTO bad FROM "playbook_instances" WHERE "workspace_id" IS NULL;
786
- IF bad <> 0 THEN
787
- RAISE EXCEPTION '#6374: % playbook_instances row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
788
- END IF;
789
-
790
- -- 1. Nothing lost — "site_listing_configs"
791
- SELECT "site_listing_configs" INTO before_n FROM "_6374_fence_census";
792
- SELECT count(*) INTO after_n FROM "site_listing_configs";
793
- IF before_n <> after_n THEN
794
- RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (site_listing_configs % -> %)', before_n, after_n;
795
- END IF;
796
-
797
- -- 2. Nothing guessed — "site_listing_configs"
798
- SELECT count(*) INTO bad FROM "site_listing_configs" x
799
- WHERE x."workspace_id" IS NOT NULL
800
- AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
801
- IF bad <> 0 THEN
802
- RAISE EXCEPTION '#6374 post-assertion: % site_listing_configs row(s) carry a workspace_id absent from workspaces', bad;
803
- END IF;
804
-
805
- -- 3. Nothing missed — "site_listing_configs"
806
- SELECT count(*) INTO bad FROM "site_listing_configs" x
807
- WHERE x."workspace_id" IS NULL
808
- AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
809
- IF bad <> 0 THEN
810
- RAISE EXCEPTION '#6374 post-assertion: % site_listing_configs row(s) had a resolvable site_id but came out of the backfill NULL', bad;
811
- END IF;
812
-
813
- -- 4. Abort on any remaining NULL — "site_listing_configs"
814
- SELECT count(*) INTO bad FROM "site_listing_configs" WHERE "workspace_id" IS NULL;
815
- IF bad <> 0 THEN
816
- RAISE EXCEPTION '#6374: % site_listing_configs row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
817
- END IF;
818
-
819
- END $$;
820
-
821
- -- ── Step 5a (D1 step 5) — SET NOT NULL (spec FR1, AC-1) ───────────────────
822
- ALTER TABLE "subscribers" ALTER COLUMN "workspace_id" SET NOT NULL;
823
- ALTER TABLE "callbacks" ALTER COLUMN "workspace_id" SET NOT NULL;
824
- ALTER TABLE "consent_grants" ALTER COLUMN "workspace_id" SET NOT NULL;
825
- ALTER TABLE "appointments" ALTER COLUMN "workspace_id" SET NOT NULL;
826
- ALTER TABLE "deliverable_assets" ALTER COLUMN "workspace_id" SET NOT NULL;
827
- ALTER TABLE "flow_checkouts" ALTER COLUMN "workspace_id" SET NOT NULL;
828
- ALTER TABLE "waitlist_experiments" ALTER COLUMN "workspace_id" SET NOT NULL;
829
- ALTER TABLE "waitlist_scores" ALTER COLUMN "workspace_id" SET NOT NULL;
830
- ALTER TABLE "waitlist_followup_sends" ALTER COLUMN "workspace_id" SET NOT NULL;
831
- ALTER TABLE "playbook_instances" ALTER COLUMN "workspace_id" SET NOT NULL;
832
- ALTER TABLE "site_listing_configs" ALTER COLUMN "workspace_id" SET NOT NULL;
833
-
834
- -- ── Step 5b (D1 step 5) — the two CHECKs, DROP then ADD, never the guarded DO block ─
835
- -- keystone D5 / SEC-06: an existence guard would silently no-op a TIGHTENING
836
- -- on any database that already carries a same-named constraint — a
837
- -- false-green control. The format CHECK carries NO `IS NULL OR` arm (the
838
- -- precedent's did, because the precedent never enforced NOT NULL; step 4
839
- -- just did).
840
- ALTER TABLE "subscribers" DROP CONSTRAINT IF EXISTS "subscribers_workspace_id_format";
841
- ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_workspace_id_format"
842
- 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}$');
843
- ALTER TABLE "subscribers" DROP CONSTRAINT IF EXISTS "subscribers_workspace_site_agree";
844
- ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_workspace_site_agree"
845
- CHECK ("workspace_id" = "site_id");
846
-
847
- ALTER TABLE "callbacks" DROP CONSTRAINT IF EXISTS "callbacks_workspace_id_format";
848
- ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_workspace_id_format"
849
- 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}$');
850
- ALTER TABLE "callbacks" DROP CONSTRAINT IF EXISTS "callbacks_workspace_site_agree";
851
- ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_workspace_site_agree"
852
- CHECK ("workspace_id" = "site_id");
853
-
854
- ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "consent_grants_workspace_id_format";
855
- ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_workspace_id_format"
856
- 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}$');
857
- ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "consent_grants_workspace_site_agree";
858
- ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_workspace_site_agree"
859
- CHECK ("workspace_id" = "site_id");
860
-
861
- ALTER TABLE "appointments" DROP CONSTRAINT IF EXISTS "appointments_workspace_id_format";
862
- ALTER TABLE "appointments" ADD CONSTRAINT "appointments_workspace_id_format"
863
- 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}$');
864
- ALTER TABLE "appointments" DROP CONSTRAINT IF EXISTS "appointments_workspace_site_agree";
865
- ALTER TABLE "appointments" ADD CONSTRAINT "appointments_workspace_site_agree"
866
- CHECK ("workspace_id" = "site_id");
867
-
868
- ALTER TABLE "deliverable_assets" DROP CONSTRAINT IF EXISTS "deliverable_assets_workspace_id_format";
869
- ALTER TABLE "deliverable_assets" ADD CONSTRAINT "deliverable_assets_workspace_id_format"
870
- 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}$');
871
- ALTER TABLE "deliverable_assets" DROP CONSTRAINT IF EXISTS "deliverable_assets_workspace_site_agree";
872
- ALTER TABLE "deliverable_assets" ADD CONSTRAINT "deliverable_assets_workspace_site_agree"
873
- CHECK ("workspace_id" = "site_id");
874
-
875
- ALTER TABLE "flow_checkouts" DROP CONSTRAINT IF EXISTS "flow_checkouts_workspace_id_format";
876
- ALTER TABLE "flow_checkouts" ADD CONSTRAINT "flow_checkouts_workspace_id_format"
877
- 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}$');
878
- ALTER TABLE "flow_checkouts" DROP CONSTRAINT IF EXISTS "flow_checkouts_workspace_site_agree";
879
- ALTER TABLE "flow_checkouts" ADD CONSTRAINT "flow_checkouts_workspace_site_agree"
880
- CHECK ("workspace_id" = "site_id");
881
-
882
- ALTER TABLE "waitlist_experiments" DROP CONSTRAINT IF EXISTS "waitlist_experiments_workspace_id_format";
883
- ALTER TABLE "waitlist_experiments" ADD CONSTRAINT "waitlist_experiments_workspace_id_format"
884
- 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}$');
885
- ALTER TABLE "waitlist_experiments" DROP CONSTRAINT IF EXISTS "waitlist_experiments_workspace_site_agree";
886
- ALTER TABLE "waitlist_experiments" ADD CONSTRAINT "waitlist_experiments_workspace_site_agree"
887
- CHECK ("workspace_id" = "site_id");
888
-
889
- ALTER TABLE "waitlist_scores" DROP CONSTRAINT IF EXISTS "waitlist_scores_workspace_id_format";
890
- ALTER TABLE "waitlist_scores" ADD CONSTRAINT "waitlist_scores_workspace_id_format"
891
- 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}$');
892
- ALTER TABLE "waitlist_scores" DROP CONSTRAINT IF EXISTS "waitlist_scores_workspace_site_agree";
893
- ALTER TABLE "waitlist_scores" ADD CONSTRAINT "waitlist_scores_workspace_site_agree"
894
- CHECK ("workspace_id" = "site_id");
895
-
896
- ALTER TABLE "waitlist_followup_sends" DROP CONSTRAINT IF EXISTS "waitlist_followup_sends_workspace_id_format";
897
- ALTER TABLE "waitlist_followup_sends" ADD CONSTRAINT "waitlist_followup_sends_workspace_id_format"
898
- 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}$');
899
- ALTER TABLE "waitlist_followup_sends" DROP CONSTRAINT IF EXISTS "waitlist_followup_sends_workspace_site_agree";
900
- ALTER TABLE "waitlist_followup_sends" ADD CONSTRAINT "waitlist_followup_sends_workspace_site_agree"
901
- CHECK ("workspace_id" = "site_id");
902
-
903
- ALTER TABLE "playbook_instances" DROP CONSTRAINT IF EXISTS "playbook_instances_workspace_id_format";
904
- ALTER TABLE "playbook_instances" ADD CONSTRAINT "playbook_instances_workspace_id_format"
905
- 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}$');
906
- ALTER TABLE "playbook_instances" DROP CONSTRAINT IF EXISTS "playbook_instances_workspace_site_agree";
907
- ALTER TABLE "playbook_instances" ADD CONSTRAINT "playbook_instances_workspace_site_agree"
908
- CHECK ("workspace_id" = "site_id");
909
-
910
- ALTER TABLE "site_listing_configs" DROP CONSTRAINT IF EXISTS "site_listing_configs_workspace_id_format";
911
- ALTER TABLE "site_listing_configs" ADD CONSTRAINT "site_listing_configs_workspace_id_format"
912
- 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}$');
913
- ALTER TABLE "site_listing_configs" DROP CONSTRAINT IF EXISTS "site_listing_configs_workspace_site_agree";
914
- ALTER TABLE "site_listing_configs" ADD CONSTRAINT "site_listing_configs_workspace_site_agree"
915
- CHECK ("workspace_id" = "site_id");
916
-
917
-
918
- -- ── D2 — `listing_csv_sends`: NOT NULL + format CHECK, no equality CHECK ────
919
- -- It has no `site_id`, so an equality CHECK has nothing to compare. The
920
- -- backfill takes the workspace from the PARENT listing row, which the
921
- -- keystone above already fenced. A composite FK to `project_listings` is
922
- -- legal here (both models are `inbox`) and was offered; the operator chose
923
- -- ONE fence shape over a second, stricter one for an audit-log table
924
- -- (2026-09-20, spec § 8 Q1 -> option (a)).
925
- ALTER TABLE "listing_csv_sends" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
926
- CREATE INDEX IF NOT EXISTS "listing_csv_sends_workspace_id_idx"
927
- ON "listing_csv_sends" ("workspace_id");
928
-
929
- UPDATE "listing_csv_sends" s
930
- SET "workspace_id" = p."workspace_id"
931
- FROM "project_listings" p
932
- WHERE s."workspace_id" IS NULL
933
- AND p."id" = s."listing_id";
934
-
935
- DO $$
936
- DECLARE
937
- bad bigint;
938
- BEGIN
939
- -- The same abort-on-NULL rule the eleven take: a send row whose listing is
940
- -- gone (or was never fenced) has no correct workspace to write, and this
941
- -- is about to SET NOT NULL. Never deleted, never guessed (NFR3).
942
- SELECT count(*) INTO bad FROM "listing_csv_sends" WHERE "workspace_id" IS NULL;
943
- IF bad <> 0 THEN
944
- RAISE EXCEPTION '#6374: % listing_csv_sends row(s) have a listing_id that resolves to no project_listings row — resolve or remove them before re-running this migration', bad;
945
- END IF;
946
- END $$;
947
-
948
- ALTER TABLE "listing_csv_sends" ALTER COLUMN "workspace_id" SET NOT NULL;
949
-
950
- ALTER TABLE "listing_csv_sends" DROP CONSTRAINT IF EXISTS "listing_csv_sends_workspace_id_format";
951
- ALTER TABLE "listing_csv_sends" ADD CONSTRAINT "listing_csv_sends_workspace_id_format"
952
- 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}$');
953
-
954
- -- ── D3 — `site_settings`: keyed BY the workspace, read through the alias ────
955
- -- It has no `site_id` either, and its one read
956
- -- (apps/api/src/services/dunning-policy.ts) arrives with a site ALIAS. Every
957
- -- existing row backfills to the SENTINEL workspace: a pre-fence row belonged
958
- -- to "the site", which is exactly what the alias `default` resolves to — the
959
- -- same rule step 1 applies to `site_id` on the eleven.
960
- --
961
- -- This is the one block that moves a UNIQUE INDEX, which is why index DDL
962
- -- appears here and in none of the five steps above: `key` was globally
963
- -- unique, so two sellers could not both hold `feature_flags`. The `key` TEXT
964
- -- itself is left VERBATIM — `dunning-policy.ts` embeds the site alias in the
965
- -- key string, and rebuilding it from the resolved id would match no row and
966
- -- silently hand every seller the built-in default policy (spec D3).
967
- ALTER TABLE "site_settings" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
968
- CREATE INDEX IF NOT EXISTS "site_settings_workspace_id_idx"
969
- ON "site_settings" ("workspace_id");
970
-
971
- -- ── ABORT, NEVER GUESS — before the sentinel assignment below (AC-14) ──────
972
- -- NFR3 ("an unresolvable row aborts the migration; it is never deleted and
973
- -- never guessed") applied to the ONE table whose tenant lives in its key TEXT
974
- -- rather than in a column. The assignment below attributes every unattributed
975
- -- row to the sentinel, which is right for a row that belonged to "the site" —
976
- -- and WRONG for a row whose key already names a DIFFERENT workspace. Such a
977
- -- row would be silently misfiled under the sentinel, and
978
- -- `dunning-policy.ts`'s read (keyed on the resolved workspace AND the verbatim
979
- -- key) would then miss it forever, falling back to the built-in default
980
- -- policy — MORE permissive than a configured one, on a delinquent-
981
- -- subscription path. Found by this unit's own security review; disposed by
982
- -- the operator 2026-09-20 as abort-not-derive.
983
- --
984
- -- This DETECTS; it never DERIVES. The embedded id is matched and counted, and
985
- -- is never read back out and used as the backfill value — key parsing is
986
- -- exactly what spec D3 forbids, because the alias in the key is load-bearing
987
- -- for the read. An operator whose database trips this resolves or removes the
988
- -- rows by hand and re-runs.
989
- --
990
- -- The negative lookahead is what makes this precise: it matches a well-formed
991
- -- `wksp_`+UUIDv7 that is NOT the sentinel literal. A key carrying the sentinel
992
- -- id, a bare alias (`…:default`), or no id at all is left for the sentinel
993
- -- assignment below, exactly as before.
994
- --
995
- -- Guarded on `workspace_id IS NULL`, so on an already-migrated database every
996
- -- row is attributed, the count is zero, and a verbatim re-run stays the no-op
997
- -- NFR2 requires.
998
- DO $$
999
- DECLARE
1000
- bad bigint;
1001
- BEGIN
1002
- SELECT count(*) INTO bad
1003
- FROM "site_settings"
1004
- WHERE "workspace_id" IS NULL
1005
- AND "key" ~ 'wksp_(?!00000000-0000-7000-8000-000000000001)[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
1006
- IF bad <> 0 THEN
1007
- RAISE EXCEPTION '#6374: % site_settings row(s) carry a NON-SENTINEL workspace id embedded in their key and no workspace_id of their own — assigning them to the sentinel would misfile them and make their read miss forever; resolve or remove them before re-running this migration', bad;
1008
- END IF;
1009
- END $$;
1010
-
1011
- UPDATE "site_settings"
1012
- SET "workspace_id" = 'wksp_00000000-0000-7000-8000-000000000001'
1013
- WHERE "workspace_id" IS NULL
1014
- AND EXISTS (
1015
- SELECT 1 FROM "workspaces" w
1016
- WHERE w."id" = 'wksp_00000000-0000-7000-8000-000000000001'
1017
- );
1018
-
1019
- DO $$
1020
- DECLARE
1021
- bad bigint;
1022
- BEGIN
1023
- -- Abort rather than guess: a settings row on a database with no sentinel
1024
- -- workspace has no workspace to belong to, and this is about to
1025
- -- SET NOT NULL.
1026
- SELECT count(*) INTO bad FROM "site_settings" WHERE "workspace_id" IS NULL;
1027
- IF bad <> 0 THEN
1028
- RAISE EXCEPTION '#6374: % site_settings row(s) remain unattributed — this database holds no sentinel workspace to backfill them to; resolve or remove them before re-running this migration', bad;
1029
- END IF;
1030
- END $$;
1031
-
1032
- ALTER TABLE "site_settings" ALTER COLUMN "workspace_id" SET NOT NULL;
1033
-
1034
- ALTER TABLE "site_settings" DROP CONSTRAINT IF EXISTS "site_settings_workspace_id_format";
1035
- ALTER TABLE "site_settings" ADD CONSTRAINT "site_settings_workspace_id_format"
1036
- 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}$');
1037
-
1038
- -- The unique key moves from (key) to (workspace_id, key).
1039
- --
1040
- -- DROP-then-CREATE for the REPLACEMENT too, never `CREATE UNIQUE INDEX IF NOT
1041
- -- EXISTS` — the same keystone D5 / SEC-06 rule the CHECKs above follow, and
1042
- -- this is the one place in this file where a constraint REPLACES a dropped
1043
- -- one, so it is the one place the false-green would actually cost something.
1044
- -- `IF NOT EXISTS` matches on NAME ALONE: an index of this name that was
1045
- -- non-unique, or over different columns, would silently satisfy the guard and
1046
- -- leave `site_settings` with NO uniqueness at all — having already dropped the
1047
- -- old one unconditionally on the line above. The old index is a plain index,
1048
- -- not a table constraint (20260508000000_add_inbox_namespace created it with
1049
- -- `CREATE UNIQUE INDEX`), so `DROP INDEX` is the correct verb for both.
1050
- DROP INDEX IF EXISTS "site_settings_key_key";
1051
- DROP INDEX IF EXISTS "site_settings_workspace_id_key_key";
1052
- CREATE UNIQUE INDEX "site_settings_workspace_id_key_key"
1053
- ON "site_settings" ("workspace_id", "key");
1054
-
1055
- -- ── D4 — `deliverable_grants`: stamped at mint, and STILL nullable ──────────
1056
- -- No column is added and no existing row is touched. NULL remains the serve
1057
- -- path's fail-closed sentinel (#6254 § 10.1 Q2): a legacy `dasset_` grant and
1058
- -- a `playbook:` virtual one keep their NULL and keep meaning "serve nothing".
1059
- -- `deliverable_grants.site_id` is deliberately NOT normalized — a legacy row
1060
- -- keeps its alias, which with a NULL workspace still means serve nothing.
1061
- --
1062
- -- What IS added is the NULL-TOLERANT equality CHECK, so a STAMPED grant can
1063
- -- never disagree with its site while a legacy grant stays storable. Both mint
1064
- -- sites stamp the workspace they already hold (AC-8).
1065
- ALTER TABLE "deliverable_grants" DROP CONSTRAINT IF EXISTS "deliverable_grants_workspace_site_agree";
1066
- ALTER TABLE "deliverable_grants" ADD CONSTRAINT "deliverable_grants_workspace_site_agree"
1067
- CHECK ("workspace_id" IS NULL OR "workspace_id" = "site_id");