@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/ask/index.d.ts +6 -0
  5. package/dist/ask/index.js +28 -0
  6. package/dist/ask/question-schema.d.ts +121 -0
  7. package/dist/ask/question-schema.js +167 -0
  8. package/dist/ask/report.d.ts +88 -0
  9. package/dist/ask/report.js +170 -0
  10. package/dist/ask/scorer.d.ts +104 -0
  11. package/dist/ask/scorer.js +164 -0
  12. package/dist/ask-run/ask-command.d.ts +62 -0
  13. package/dist/ask-run/ask-command.js +254 -0
  14. package/dist/ask-run/host/app.d.ts +65 -0
  15. package/dist/ask-run/host/app.js +146 -0
  16. package/dist/ask-run/host/db.d.ts +38 -0
  17. package/dist/ask-run/host/db.js +126 -0
  18. package/dist/ask-run/host/ports.d.ts +35 -0
  19. package/dist/ask-run/host/ports.js +113 -0
  20. package/dist/ask-run/index.d.ts +13 -0
  21. package/dist/ask-run/index.js +35 -0
  22. package/dist/ask-run/replay.d.ts +128 -0
  23. package/dist/ask-run/replay.js +212 -0
  24. package/dist/ask-run/run-ask-eval.d.ts +125 -0
  25. package/dist/ask-run/run-ask-eval.js +193 -0
  26. package/dist/ask-run/seed-golden-graph.d.ts +64 -0
  27. package/dist/ask-run/seed-golden-graph.js +495 -0
  28. package/dist/cli/index.d.ts +3 -0
  29. package/dist/cli/index.js +12 -0
  30. package/dist/cli/kg-command.d.ts +66 -0
  31. package/dist/cli/kg-command.js +238 -0
  32. package/dist/cli/main.d.ts +18 -0
  33. package/dist/cli/main.js +69 -0
  34. package/dist/fixtures/domains.d.ts +16 -0
  35. package/dist/fixtures/domains.js +33 -0
  36. package/dist/fixtures/fixture-schema.d.ts +323 -0
  37. package/dist/fixtures/fixture-schema.js +199 -0
  38. package/dist/fixtures/index.d.ts +16 -0
  39. package/dist/fixtures/index.js +40 -0
  40. package/dist/fixtures/load.d.ts +73 -0
  41. package/dist/fixtures/load.js +137 -0
  42. package/dist/fixtures/questions.d.ts +15 -0
  43. package/dist/fixtures/questions.js +26 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +36 -0
  46. package/dist/kg/gold-runner.d.ts +13 -0
  47. package/dist/kg/gold-runner.js +57 -0
  48. package/dist/kg/index.d.ts +17 -0
  49. package/dist/kg/index.js +65 -0
  50. package/dist/kg/internals.d.ts +7 -0
  51. package/dist/kg/internals.js +17 -0
  52. package/dist/kg/prediction-schema.d.ts +245 -0
  53. package/dist/kg/prediction-schema.js +102 -0
  54. package/dist/kg/predictions.d.ts +11 -0
  55. package/dist/kg/predictions.js +49 -0
  56. package/dist/kg/report.d.ts +84 -0
  57. package/dist/kg/report.js +125 -0
  58. package/dist/kg/run-kg-eval.d.ts +77 -0
  59. package/dist/kg/run-kg-eval.js +137 -0
  60. package/dist/kg/runner.d.ts +72 -0
  61. package/dist/kg/runner.js +51 -0
  62. package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
  63. package/dist/kg/runners/extract-resolve-runner.js +199 -0
  64. package/dist/kg/runners/extract-runner.d.ts +65 -0
  65. package/dist/kg/runners/extract-runner.js +243 -0
  66. package/dist/kg/scorer.d.ts +226 -0
  67. package/dist/kg/scorer.js +463 -0
  68. package/dist/kg/targets.d.ts +17 -0
  69. package/dist/kg/targets.js +16 -0
  70. package/dist/kg/text.d.ts +45 -0
  71. package/dist/kg/text.js +100 -0
  72. package/dist/live/adapters.d.ts +16 -0
  73. package/dist/live/adapters.js +58 -0
  74. package/dist/live/env.d.ts +39 -0
  75. package/dist/live/env.js +56 -0
  76. package/dist/live/index.d.ts +6 -0
  77. package/dist/live/index.js +16 -0
  78. package/dist/live/live-ai.d.ts +104 -0
  79. package/dist/live/live-ai.js +157 -0
  80. package/dist/parity/constants.d.ts +79 -0
  81. package/dist/parity/constants.js +104 -0
  82. package/dist/parity/index.d.ts +20 -0
  83. package/dist/parity/index.js +38 -0
  84. package/dist/parity/internals.d.ts +8 -0
  85. package/dist/parity/internals.js +16 -0
  86. package/dist/parity/scenario.d.ts +45 -0
  87. package/dist/parity/scenario.js +245 -0
  88. package/dist/parity/script.d.ts +44 -0
  89. package/dist/parity/script.js +136 -0
  90. package/dist/parity/snapshot.d.ts +43 -0
  91. package/dist/parity/snapshot.js +71 -0
  92. package/dist/real-data/index.d.ts +2 -0
  93. package/dist/real-data/index.js +12 -0
  94. package/dist/real-data/real-data.d.ts +76 -0
  95. package/dist/real-data/real-data.js +156 -0
  96. package/fixtures/kg-golden/README.md +136 -0
  97. package/fixtures/kg-golden/ask-questions.json +458 -0
  98. package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
  99. package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
  100. package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
  101. package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
  102. package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
  103. package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
  104. package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
  105. package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
  106. package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
  107. package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
  108. package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
  109. package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
  110. package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
  111. package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
  112. package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
  113. package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
  114. package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
  115. package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
  116. package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
  117. package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
  118. package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
  119. package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
  120. package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
  121. package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
  122. package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
  123. package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
  124. package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
  125. package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
  126. package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
  127. package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
  128. package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
  129. package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
  130. package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
  131. package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
  132. package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
  133. package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
  134. package/host/generated/client.d.ts +1 -0
  135. package/host/generated/client.js +5 -0
  136. package/host/generated/default.d.ts +1 -0
  137. package/host/generated/default.js +5 -0
  138. package/host/generated/edge.d.ts +1 -0
  139. package/host/generated/edge.js +1658 -0
  140. package/host/generated/index-browser.js +1684 -0
  141. package/host/generated/index.d.ts +192335 -0
  142. package/host/generated/index.js +1658 -0
  143. package/host/generated/package.json +139 -0
  144. package/host/generated/query_compiler_fast_bg.js +2 -0
  145. package/host/generated/query_compiler_fast_bg.wasm +0 -0
  146. package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
  147. package/host/generated/runtime/client.d.ts +3433 -0
  148. package/host/generated/runtime/client.js +110 -0
  149. package/host/generated/runtime/index-browser.d.ts +90 -0
  150. package/host/generated/runtime/index-browser.js +6 -0
  151. package/host/generated/runtime/wasm-compiler-edge.js +100 -0
  152. package/host/generated/schema.prisma +6312 -0
  153. package/host/generated/wasm-edge-light-loader.mjs +5 -0
  154. package/host/generated/wasm-worker-loader.mjs +5 -0
  155. package/host/prisma/fragments/kvox.ask.prisma +126 -0
  156. package/host/prisma/fragments/kvox.graph.prisma +1073 -0
  157. package/host/prisma/fragments/kvox.notes.prisma +787 -0
  158. package/host/prisma/fragments/kvox.search.prisma +211 -0
  159. package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
  160. package/host/prisma/kvox.lock +101 -0
  161. package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
  162. package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
  163. package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
  164. package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
  165. package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
  166. package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
  167. package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
  168. package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
  169. package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
  170. package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
  171. package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
  172. package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
  173. package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
  174. package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
  175. package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
  176. package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
  177. package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
  178. package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
  179. package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
  180. package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
  181. package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
  182. package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
  183. package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
  184. package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
  185. package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
  186. package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
  187. package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
  188. package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
  189. package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
  190. package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
  191. package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
  192. package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
  193. package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
  194. package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
  195. package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
  196. package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
  197. package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
  198. package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
  199. package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
  200. package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
  201. package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
  202. package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
  203. package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
  204. package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
  205. package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
  206. package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
  207. package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
  208. package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
  209. package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
  210. package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
  211. package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
  212. package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
  213. package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
  214. package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
  215. package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
  216. package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
  217. package/host/prisma/platform.lock +319 -0
  218. package/host/prisma/schema/app.kvox.ask.prisma +110 -0
  219. package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
  220. package/host/prisma/schema/app.kvox.notes.prisma +746 -0
  221. package/host/prisma/schema/app.kvox.search.prisma +208 -0
  222. package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
  223. package/host/prisma/schema/platform.ai.prisma +419 -0
  224. package/host/prisma/schema/platform.android-app.prisma +46 -0
  225. package/host/prisma/schema/platform.base.prisma +15 -0
  226. package/host/prisma/schema/platform.credentials.prisma +156 -0
  227. package/host/prisma/schema/platform.db-backup.prisma +359 -0
  228. package/host/prisma/schema/platform.identity.prisma +638 -0
  229. package/host/prisma/schema/platform.jobs.prisma +824 -0
  230. package/host/prisma/schema/platform.notifications.prisma +641 -0
  231. package/host/prisma/schema/platform.settings.prisma +53 -0
  232. package/host/prisma/schema/platform.sharing.prisma +208 -0
  233. package/host/prisma/schema/platform.storage.prisma +118 -0
  234. package/package.json +112 -1
  235. package/parity/requests.ask.json +2681 -0
  236. package/parity/requests.m01.json +1554 -0
  237. package/parity/requests.m02.json +1554 -0
  238. package/parity/requests.m03.json +1554 -0
  239. package/parity/requests.m04.json +1554 -0
  240. package/parity/requests.m05.json +1554 -0
  241. package/parity/requests.m06.json +1554 -0
  242. package/parity/requests.m07.json +1554 -0
  243. package/parity/requests.m08.json +1554 -0
  244. package/parity/requests.m09.json +1554 -0
  245. package/parity/requests.m10.json +1554 -0
@@ -0,0 +1,32 @@
1
+ -- =============================================================================
2
+ -- Revoke `ai:use` from the Viewer role on existing deployments (issue #499)
3
+ -- =============================================================================
4
+ -- Viewer is the DEFAULT role every new user lands in (see `ROLES` in
5
+ -- `prisma/seed-data.ts` and the allowlist-driven signup path). Seeding
6
+ -- `ai:use` onto it (issue #423, epic #419) meant every fresh signup could
7
+ -- call AI with no explicit grant — harmless under the `byok` key policy (no
8
+ -- key, no calls succeed), but wrong under `byok_with_org_fallback`: a
9
+ -- brand-new Viewer would silently spend the deployment's own org key the
10
+ -- first time they touched an AI surface, with no administrator having
11
+ -- decided that person should be able to.
12
+ --
13
+ -- This migration is DATA-ONLY (no `schema.prisma` change — the shape of
14
+ -- `role_permissions` is unchanged) and undoes exactly that one grant on
15
+ -- deployments that already ran the old seed: it deletes the single
16
+ -- `role_permissions` row joining the role named 'viewer' and the permission
17
+ -- named 'ai:use', and touches nothing else — not Admin's or Contributor's
18
+ -- grants, not any other Viewer permission.
19
+ --
20
+ -- IDEMPOTENT: a `DELETE ... WHERE` naming both rows by subquery is a no-op if
21
+ -- the row is already gone, or if either the 'viewer' role or the 'ai:use'
22
+ -- permission does not exist at all (a fork that renamed or removed either).
23
+ -- Safe to run against a database that never had the row (a fresh install
24
+ -- seeded from the already-updated `seed-data.ts`) and safe to re-run.
25
+ --
26
+ -- An administrator who wants a specific Viewer (or all of them) to use AI
27
+ -- again grants it back explicitly — re-insert a `role_permissions` row for
28
+ -- `('viewer', 'ai:use')` — or promotes the account to Contributor, which
29
+ -- still carries the grant.
30
+ DELETE FROM "role_permissions"
31
+ WHERE "role_id" = (SELECT "id" FROM "roles" WHERE "name" = 'viewer')
32
+ AND "permission_id" = (SELECT "id" FROM "permissions" WHERE "name" = 'ai:use');
@@ -0,0 +1,29 @@
1
+ -- =============================================================================
2
+ -- Per-user encrypted credentials (issue #387)
3
+ -- =============================================================================
4
+ -- A sibling of "credentials", which is NOT modified. Rows are addressed by
5
+ -- (user_id, purpose, name); `secret` is ciphertext encrypted under the
6
+ -- owner-bound sub-key domain `user:<user_id>:<purpose>`. All three unique
7
+ -- columns are NOT NULL so the unique index is a real constraint. Rows cascade
8
+ -- with their owner.
9
+
10
+ -- CreateTable
11
+ CREATE TABLE "user_credentials" (
12
+ "id" UUID NOT NULL,
13
+ "user_id" UUID NOT NULL,
14
+ "purpose" TEXT NOT NULL,
15
+ "name" TEXT NOT NULL,
16
+ "secret" TEXT NOT NULL,
17
+ "hint" TEXT,
18
+ "label" TEXT,
19
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ "updated_at" TIMESTAMPTZ NOT NULL,
21
+
22
+ CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id")
23
+ );
24
+
25
+ -- CreateIndex
26
+ CREATE UNIQUE INDEX "user_credentials_user_id_purpose_name_key" ON "user_credentials"("user_id", "purpose", "name");
27
+
28
+ -- AddForeignKey
29
+ ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,52 @@
1
+ -- =============================================================================
2
+ -- Device session <-> issued credential link (issue #518)
3
+ -- =============================================================================
4
+ -- Today nothing links a `DeviceCode` row to what it issued: once the polling
5
+ -- client collects a PAT (and that PAT is used to mint refresh tokens),
6
+ -- revoking the `DeviceCode` "session" does nothing to the credential the
7
+ -- device is actually holding. This migration adds the columns that let a
8
+ -- device-session revocation reach both:
9
+ --
10
+ -- 1. `device_codes.pat_id`: the PAT minted for this device once collected.
11
+ -- Nullable until collection. `SetNull` on delete — a `DeviceCode` history
12
+ -- row must survive its PAT being deleted (or aging out) independently.
13
+ -- 2. `device_codes.collected_at` / `credential_expires_at`: collection state
14
+ -- copied from the PAT at collection time, so the sessions list can filter
15
+ -- "approved, not yet collected" vs. "collected, still valid" without a
16
+ -- join against `personal_access_tokens`.
17
+ -- 3. `device_codes.revoked_at`: marks the device session (and whatever it
18
+ -- issued) as revoked. Distinct from `PersonalAccessToken.revokedAt` /
19
+ -- `RefreshToken.revokedAt`, which the revocation service also sets on the
20
+ -- linked rows — this column is what the sessions list itself filters on.
21
+ -- 4. `refresh_tokens.device_code_id`: the device-authorization session that
22
+ -- minted this refresh token, if any, so that same revocation can reach
23
+ -- refresh tokens too. `SetNull` on delete for the same reason as above.
24
+ --
25
+ -- Additive only: every new column is nullable, no backfill, no data migration.
26
+ -- =============================================================================
27
+
28
+ -- AlterTable
29
+ ALTER TABLE "device_codes"
30
+ ADD COLUMN "pat_id" UUID,
31
+ ADD COLUMN "collected_at" TIMESTAMPTZ,
32
+ ADD COLUMN "credential_expires_at" TIMESTAMPTZ,
33
+ ADD COLUMN "revoked_at" TIMESTAMPTZ;
34
+
35
+ -- AlterTable
36
+ ALTER TABLE "refresh_tokens" ADD COLUMN "device_code_id" UUID;
37
+
38
+ -- CreateIndex
39
+ -- Serves the sessions list: "this user's device sessions that are not
40
+ -- revoked" (approved-uncollected and collected-unexpired both satisfy
41
+ -- `revoked_at IS NULL`); the remaining collected/expiry filtering is done in
42
+ -- the query predicate, not a second index.
43
+ CREATE INDEX "device_codes_user_id_revoked_at_idx" ON "device_codes"("user_id", "revoked_at");
44
+
45
+ -- CreateIndex
46
+ CREATE INDEX "refresh_tokens_device_code_id_idx" ON "refresh_tokens"("device_code_id");
47
+
48
+ -- AddForeignKey
49
+ ALTER TABLE "device_codes" ADD CONSTRAINT "device_codes_pat_id_fkey" FOREIGN KEY ("pat_id") REFERENCES "personal_access_tokens"("id") ON DELETE SET NULL ON UPDATE CASCADE;
50
+
51
+ -- AddForeignKey
52
+ ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_device_code_id_fkey" FOREIGN KEY ("device_code_id") REFERENCES "device_codes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
1
+ -- AlterTable
2
+ ALTER TABLE "worker_nodes" ADD COLUMN "last_vitals" JSONB,
3
+ ADD COLUMN "last_vitals_at" TIMESTAMPTZ;
@@ -0,0 +1,6 @@
1
+ -- Issue #607: carry the enqueuing request's W3C trace context on the job row.
2
+ -- Nullable, no default, no index: existing rows simply have no trace context.
3
+ -- The partial unique index jobs_active_dedup_uniq_idx is deliberately untouched.
4
+
5
+ -- AlterTable
6
+ ALTER TABLE "jobs" ADD COLUMN "trace_context" TEXT;
@@ -0,0 +1,13 @@
1
+ -- Retention sweeps (#681): each purge reads "the oldest rows older than the
2
+ -- cutoff", across every user. Plain btree indexes on the timestamp, so a batch
3
+ -- is an index range scan rather than a sequential scan. `audit_events` already
4
+ -- has `audit_events_created_at_idx`.
5
+
6
+ -- CreateIndex
7
+ CREATE INDEX "notification_deliveries_created_at_idx" ON "notification_deliveries"("created_at");
8
+
9
+ -- CreateIndex
10
+ CREATE INDEX "notifications_created_at_idx" ON "notifications"("created_at");
11
+
12
+ -- CreateIndex
13
+ CREATE INDEX "ai_runs_created_at_idx" ON "ai_runs"("created_at");
@@ -0,0 +1,142 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "MembershipStatus" AS ENUM ('active', 'suspended');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "InviteStatus" AS ENUM ('pending', 'accepted', 'revoked', 'expired');
6
+
7
+ -- AlterTable
8
+ ALTER TABLE "device_codes" ADD COLUMN "org_id" UUID;
9
+
10
+ -- AlterTable
11
+ ALTER TABLE "personal_access_tokens" ADD COLUMN "org_id" UUID;
12
+
13
+ -- AlterTable
14
+ ALTER TABLE "refresh_tokens" ADD COLUMN "org_id" UUID;
15
+
16
+ -- CreateTable
17
+ CREATE TABLE "organizations" (
18
+ "id" UUID NOT NULL,
19
+ "name" TEXT NOT NULL,
20
+ "slug" TEXT NOT NULL,
21
+ "is_default" BOOLEAN NOT NULL DEFAULT false,
22
+ "created_by_id" UUID,
23
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+ "updated_at" TIMESTAMPTZ NOT NULL,
25
+
26
+ CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
27
+ );
28
+
29
+ -- CreateTable
30
+ CREATE TABLE "memberships" (
31
+ "id" UUID NOT NULL,
32
+ "org_id" UUID NOT NULL,
33
+ "user_id" UUID NOT NULL,
34
+ "status" "MembershipStatus" NOT NULL DEFAULT 'active',
35
+ "last_active_at" TIMESTAMPTZ,
36
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
37
+ "updated_at" TIMESTAMPTZ NOT NULL,
38
+
39
+ CONSTRAINT "memberships_pkey" PRIMARY KEY ("id")
40
+ );
41
+
42
+ -- CreateTable
43
+ CREATE TABLE "org_invites" (
44
+ "id" UUID NOT NULL,
45
+ "org_id" UUID NOT NULL,
46
+ "email" TEXT NOT NULL,
47
+ "status" "InviteStatus" NOT NULL DEFAULT 'pending',
48
+ "token_hash" TEXT,
49
+ "expires_at" TIMESTAMPTZ,
50
+ "invited_by_id" UUID,
51
+ "accepted_by_id" UUID,
52
+ "accepted_at" TIMESTAMPTZ,
53
+ "notes" TEXT,
54
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
+
56
+ CONSTRAINT "org_invites_pkey" PRIMARY KEY ("id")
57
+ );
58
+
59
+ -- CreateIndex
60
+ CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
61
+
62
+ -- CreateIndex
63
+ CREATE INDEX "memberships_user_id_idx" ON "memberships"("user_id");
64
+
65
+ -- CreateIndex
66
+ CREATE UNIQUE INDEX "memberships_org_id_user_id_key" ON "memberships"("org_id", "user_id");
67
+
68
+ -- CreateIndex
69
+ CREATE UNIQUE INDEX "org_invites_token_hash_key" ON "org_invites"("token_hash");
70
+
71
+ -- CreateIndex
72
+ CREATE INDEX "org_invites_email_idx" ON "org_invites"("email");
73
+
74
+ -- CreateIndex
75
+ CREATE UNIQUE INDEX "org_invites_org_id_email_key" ON "org_invites"("org_id", "email");
76
+
77
+ -- CreateIndex
78
+ CREATE INDEX "device_codes_org_id_idx" ON "device_codes"("org_id");
79
+
80
+ -- CreateIndex
81
+ CREATE INDEX "personal_access_tokens_org_id_idx" ON "personal_access_tokens"("org_id");
82
+
83
+ -- CreateIndex
84
+ CREATE INDEX "refresh_tokens_org_id_idx" ON "refresh_tokens"("org_id");
85
+
86
+ -- AddForeignKey
87
+ ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
88
+
89
+ -- AddForeignKey
90
+ ALTER TABLE "personal_access_tokens" ADD CONSTRAINT "personal_access_tokens_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
91
+
92
+ -- AddForeignKey
93
+ ALTER TABLE "device_codes" ADD CONSTRAINT "device_codes_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
94
+
95
+ -- AddForeignKey
96
+ ALTER TABLE "organizations" ADD CONSTRAINT "organizations_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
97
+
98
+ -- AddForeignKey
99
+ ALTER TABLE "memberships" ADD CONSTRAINT "memberships_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
100
+
101
+ -- AddForeignKey
102
+ ALTER TABLE "memberships" ADD CONSTRAINT "memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
103
+
104
+ -- AddForeignKey
105
+ ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
106
+
107
+ -- AddForeignKey
108
+ ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_invited_by_id_fkey" FOREIGN KEY ("invited_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
109
+
110
+ -- AddForeignKey
111
+ ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_accepted_by_id_fkey" FOREIGN KEY ("accepted_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
112
+
113
+ -- =============================================================================
114
+ -- Raw SQL (hand-written; Prisma cannot express a partial unique index)
115
+ -- =============================================================================
116
+
117
+ -- Exactly one default organization: a constant-expression partial unique index.
118
+ -- Intentional schema drift: never replace it with @@unique or a findFirst check.
119
+ CREATE UNIQUE INDEX "organizations_default_uniq_idx" ON "organizations" ((true)) WHERE "is_default";
120
+
121
+ -- =============================================================================
122
+ -- Backfill (forward-only). Every INSERT names its id: the server-side uuid
123
+ -- defaults were dropped (0005_drop_stale_uuid_defaults).
124
+ -- =============================================================================
125
+
126
+ -- 1. The default organization.
127
+ INSERT INTO "organizations" ("id", "name", "slug", "is_default", "created_at", "updated_at")
128
+ VALUES (gen_random_uuid(), 'Default organization', 'default', true, now(), now())
129
+ ON CONFLICT DO NOTHING;
130
+
131
+ -- 2. One active membership per existing user.
132
+ INSERT INTO "memberships" ("id", "org_id", "user_id", "status", "last_active_at", "created_at", "updated_at")
133
+ SELECT gen_random_uuid(), o."id", u."id", 'active', now(), now(), now()
134
+ FROM "users" u
135
+ CROSS JOIN "organizations" o
136
+ WHERE o."is_default"
137
+ ON CONFLICT ("org_id", "user_id") DO NOTHING;
138
+
139
+ -- 3. Every existing credential row acts in the default organization.
140
+ UPDATE "refresh_tokens" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
141
+ UPDATE "personal_access_tokens" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
142
+ UPDATE "device_codes" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
@@ -0,0 +1,131 @@
1
+ -- =============================================================================
2
+ -- Split RBAC into system roles and org roles (PP-6.3, issue #723)
3
+ -- =============================================================================
4
+ --
5
+ -- System roles operate the deployment and stay in "user_roles" (admin). Org
6
+ -- roles operate one organization and move onto the membership
7
+ -- ("memberships"."role_id"): org_admin, contributor, viewer. Every role and
8
+ -- permission gains a "scope". Effective permissions become the system roles'
9
+ -- grants plus the grants of the current organization's membership role, so the
10
+ -- data below is moved so that every user keeps exactly what they had.
11
+ --
12
+ -- Forward-only and in one transaction: the expand (columns) and the data move
13
+ -- happen together, so no request between "migrate deploy" and the seed run
14
+ -- sees an administrator without a permission. Nothing is dropped: "user_roles"
15
+ -- keeps its shape (expand/contract); only rows pointing at org roles go.
16
+ -- Every INSERT names its id (0005_drop_stale_uuid_defaults).
17
+ -- =============================================================================
18
+
19
+ -- CreateEnum
20
+ CREATE TYPE "RoleScope" AS ENUM ('system', 'org');
21
+
22
+ -- AlterTable: nullable first, backfilled below, then NOT NULL.
23
+ ALTER TABLE "memberships" ADD COLUMN "role_id" UUID;
24
+
25
+ -- AlterTable: NULL means the default org role (viewer).
26
+ ALTER TABLE "org_invites" ADD COLUMN "role_id" UUID;
27
+
28
+ -- AlterTable
29
+ ALTER TABLE "permissions" ADD COLUMN "scope" "RoleScope" NOT NULL DEFAULT 'system';
30
+
31
+ -- AlterTable
32
+ ALTER TABLE "roles" ADD COLUMN "scope" "RoleScope" NOT NULL DEFAULT 'system';
33
+
34
+ -- -----------------------------------------------------------------------------
35
+ -- 1. The org roles exist. org_admin is new; viewer is the fallback role of
36
+ -- step 4 and may be missing on a database the seed has not reached yet.
37
+ -- The seed refreshes both descriptions.
38
+ -- -----------------------------------------------------------------------------
39
+ INSERT INTO "roles" ("id", "name", "description", "scope")
40
+ VALUES (gen_random_uuid(), 'org_admin', 'Organization administrator - everything a contributor can do, plus manage the organization members and invites', 'org')
41
+ ON CONFLICT ("name") DO NOTHING;
42
+
43
+ INSERT INTO "roles" ("id", "name", "description", "scope")
44
+ VALUES (gen_random_uuid(), 'viewer', 'Read-only organization member - view content and manage own settings', 'org')
45
+ ON CONFLICT ("name") DO NOTHING;
46
+
47
+ -- -----------------------------------------------------------------------------
48
+ -- 2. Scopes. admin stays 'system' (the column default), like every
49
+ -- operational permission. An app's own roles and permissions keep the
50
+ -- default until the seed writes the scope their declarations name.
51
+ -- -----------------------------------------------------------------------------
52
+ UPDATE "roles" SET "scope" = 'org' WHERE "name" IN ('org_admin', 'contributor', 'viewer');
53
+
54
+ UPDATE "permissions" SET "scope" = 'org'
55
+ WHERE "name" IN (
56
+ 'user_settings:read', 'user_settings:write', 'storage:read', 'storage:write', 'ai:use',
57
+ 'org_members:read', 'org_members:write', 'org_invites:read', 'org_invites:write'
58
+ );
59
+
60
+ -- -----------------------------------------------------------------------------
61
+ -- 3. admin's grants of org permissions move to org_admin: copy first, then
62
+ -- delete. Every admin gets org_admin on their membership in step 4, in this
63
+ -- same transaction, so no administrator loses a permission. Grants an
64
+ -- administrator made to contributor or viewer are left as they are.
65
+ -- -----------------------------------------------------------------------------
66
+ INSERT INTO "role_permissions" ("role_id", "permission_id")
67
+ SELECT oa."id", rp."permission_id"
68
+ FROM "role_permissions" rp
69
+ JOIN "roles" a ON a."id" = rp."role_id" AND a."name" = 'admin'
70
+ JOIN "permissions" p ON p."id" = rp."permission_id" AND p."scope" = 'org'
71
+ CROSS JOIN "roles" oa
72
+ WHERE oa."name" = 'org_admin'
73
+ ON CONFLICT DO NOTHING;
74
+
75
+ DELETE FROM "role_permissions" rp
76
+ USING "roles" a, "permissions" p
77
+ WHERE rp."role_id" = a."id" AND a."name" = 'admin'
78
+ AND rp."permission_id" = p."id" AND p."scope" = 'org';
79
+
80
+ -- -----------------------------------------------------------------------------
81
+ -- 4. Every user is a member of the default organization (0023 did this for
82
+ -- the users that existed then; this catches any created since through a
83
+ -- path that skipped it), and every membership gets its role from the
84
+ -- user's global roles: admin -> org_admin; otherwise the highest of
85
+ -- contributor > viewer; none -> viewer (the default org role).
86
+ -- -----------------------------------------------------------------------------
87
+ INSERT INTO "memberships" ("id", "org_id", "user_id", "status", "last_active_at", "created_at", "updated_at")
88
+ SELECT gen_random_uuid(), o."id", u."id", 'active', now(), now(), now()
89
+ FROM "users" u
90
+ CROSS JOIN "organizations" o
91
+ WHERE o."is_default"
92
+ ON CONFLICT ("org_id", "user_id") DO NOTHING;
93
+
94
+ UPDATE "memberships" m
95
+ SET "role_id" = (
96
+ SELECT r."id" FROM "roles" r
97
+ WHERE r."name" = CASE
98
+ WHEN EXISTS (
99
+ SELECT 1 FROM "user_roles" ur JOIN "roles" x ON x."id" = ur."role_id"
100
+ WHERE ur."user_id" = m."user_id" AND x."name" = 'admin'
101
+ ) THEN 'org_admin'
102
+ WHEN EXISTS (
103
+ SELECT 1 FROM "user_roles" ur JOIN "roles" x ON x."id" = ur."role_id"
104
+ WHERE ur."user_id" = m."user_id" AND x."name" = 'contributor'
105
+ ) THEN 'contributor'
106
+ ELSE 'viewer'
107
+ END
108
+ )
109
+ WHERE m."role_id" IS NULL;
110
+
111
+ ALTER TABLE "memberships" ALTER COLUMN "role_id" SET NOT NULL;
112
+
113
+ -- -----------------------------------------------------------------------------
114
+ -- 5. Contract, in data only: the global assignments of org roles are now
115
+ -- carried by the memberships. admin rows (system) stay.
116
+ -- -----------------------------------------------------------------------------
117
+ DELETE FROM "user_roles" ur
118
+ USING "roles" r
119
+ WHERE ur."role_id" = r."id" AND r."scope" = 'org';
120
+
121
+ -- CreateIndex
122
+ CREATE INDEX "memberships_role_id_idx" ON "memberships"("role_id");
123
+
124
+ -- CreateIndex
125
+ CREATE INDEX "org_invites_role_id_idx" ON "org_invites"("role_id");
126
+
127
+ -- AddForeignKey
128
+ ALTER TABLE "memberships" ADD CONSTRAINT "memberships_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
129
+
130
+ -- AddForeignKey
131
+ ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,215 @@
1
+ -- =============================================================================
2
+ -- org_id on tenant-scoped tables and row-level security (PP-6.5, issue #725)
3
+ -- =============================================================================
4
+ --
5
+ -- The database enforces organization isolation (ADR 0002 D5). Four tables are
6
+ -- org-owned and get a NOT NULL (or, for usage events, nullable) org_id plus a
7
+ -- FORCEd policy keyed on transaction-local settings; audit_events gets a
8
+ -- nullable org_id and no policy in this release.
9
+ --
10
+ -- storage_objects org_id NOT NULL, FK RESTRICT, RLS forced
11
+ -- storage_object_chunks org_id NOT NULL, FK RESTRICT, RLS forced; its link to
12
+ -- the parent is now COMPOSITE (object_id, org_id) ->
13
+ -- storage_objects (id, org_id): a foreign-key check
14
+ -- bypasses row-level security, so a plain key would let
15
+ -- one organization store a reference to another's row
16
+ -- ai_runs org_id NOT NULL, FK RESTRICT, RLS forced. Restrict, not
17
+ -- SetNull: the column is NOT NULL, so history cannot
18
+ -- survive an organization's deletion by nulling it
19
+ -- ai_usage_events org_id nullable, FK SET NULL, RLS forced. Nullable so
20
+ -- a deployment-wide event (a catalogue sync) and the
21
+ -- history of a deleted organization survive; such a row
22
+ -- is visible to the system (bypass) client only
23
+ -- audit_events org_id nullable, FK SET NULL, NO row-level security
24
+ --
25
+ -- BACKFILL. Every existing row of the three NOT NULL tables, and of
26
+ -- ai_usage_events, goes to the default organization (created by
27
+ -- 0023_add_organizations on every database). Two exceptions keep NULL:
28
+ --
29
+ -- - ai_usage_events with operation = 'catalog': a catalogue sync is run by the
30
+ -- deployment, not by an organization, and new ones are written without one.
31
+ -- - audit_events whose action is a SYSTEM action. Everything else (including
32
+ -- an action this migration does not know) belongs to the default
33
+ -- organization. System action prefixes:
34
+ -- system_settings: email_settings: push_config: storage_config:
35
+ -- ai_config: ai_model: ai_catalog: telemetry: support_bundle:
36
+ -- maintenance: db_restore: db_backup: allowlist: users: nodes:
37
+ -- jobs. notification_broadcast. settings. retention.
38
+ --
39
+ -- The backfill runs in an explicit transaction that raises the bypass flag
40
+ -- (the pattern ADR 0002 prescribes for any migration that touches an RLS
41
+ -- table: under FORCE a bare UPDATE would silently change zero rows). The
42
+ -- policies are enabled AFTER it, so on a first apply the flag is belt and
43
+ -- braces; it keeps the pattern in front of the next reader.
44
+ --
45
+ -- THE APPLICATION ROLE MUST BE NOSUPERUSER NOBYPASSRLS, or every policy below is
46
+ -- inert (a superuser sees every row of a FORCEd table). The block at the end
47
+ -- warns when the role applying this migration is not an ordinary role. The
48
+ -- Doctor check `db.rls_role` fails while any FORCEd table exists and the
49
+ -- application role is a superuser or has BYPASSRLS.
50
+ --
51
+ -- DEPLOYMENT NOTE. The statements below take short locks and build the new
52
+ -- indexes inside the migration transaction; on a very large storage_objects or
53
+ -- ai_usage_events table pre-create the indexes with CREATE INDEX CONCURRENTLY
54
+ -- IF NOT EXISTS under the same names before applying.
55
+ -- =============================================================================
56
+
57
+ -- DropForeignKey
58
+ ALTER TABLE "storage_object_chunks" DROP CONSTRAINT "storage_object_chunks_object_id_fkey";
59
+
60
+ -- AlterTable (expand: nullable first, so existing rows can be backfilled)
61
+ ALTER TABLE "storage_objects" ADD COLUMN "org_id" UUID;
62
+ ALTER TABLE "storage_object_chunks" ADD COLUMN "org_id" UUID;
63
+ ALTER TABLE "ai_runs" ADD COLUMN "org_id" UUID;
64
+ ALTER TABLE "ai_usage_events" ADD COLUMN "org_id" UUID;
65
+ ALTER TABLE "audit_events" ADD COLUMN "org_id" UUID;
66
+
67
+ -- Backfill, as system work.
68
+ BEGIN;
69
+ SELECT set_config('app.rls_bypass', 'on', true);
70
+
71
+ UPDATE "storage_objects"
72
+ SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
73
+ WHERE "org_id" IS NULL;
74
+
75
+ UPDATE "storage_object_chunks" AS c
76
+ SET "org_id" = o."org_id"
77
+ FROM "storage_objects" AS o
78
+ WHERE c."object_id" = o."id" AND c."org_id" IS NULL;
79
+
80
+ UPDATE "ai_runs"
81
+ SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
82
+ WHERE "org_id" IS NULL;
83
+
84
+ UPDATE "ai_usage_events"
85
+ SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
86
+ WHERE "org_id" IS NULL AND "operation" <> 'catalog';
87
+
88
+ UPDATE "audit_events"
89
+ SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
90
+ WHERE "org_id" IS NULL
91
+ AND "action" NOT LIKE 'system\_settings:%'
92
+ AND "action" NOT LIKE 'email\_settings:%'
93
+ AND "action" NOT LIKE 'push\_config:%'
94
+ AND "action" NOT LIKE 'storage\_config:%'
95
+ AND "action" NOT LIKE 'ai\_config:%'
96
+ AND "action" NOT LIKE 'ai\_model:%'
97
+ AND "action" NOT LIKE 'ai\_catalog:%'
98
+ AND "action" NOT LIKE 'telemetry:%'
99
+ AND "action" NOT LIKE 'support\_bundle:%'
100
+ AND "action" NOT LIKE 'maintenance:%'
101
+ AND "action" NOT LIKE 'db\_restore:%'
102
+ AND "action" NOT LIKE 'db\_backup:%'
103
+ AND "action" NOT LIKE 'allowlist:%'
104
+ AND "action" NOT LIKE 'users:%'
105
+ AND "action" NOT LIKE 'nodes:%'
106
+ AND "action" NOT LIKE 'jobs.%'
107
+ AND "action" NOT LIKE 'notification\_broadcast.%'
108
+ AND "action" NOT LIKE 'settings.%'
109
+ AND "action" NOT LIKE 'retention.%';
110
+ COMMIT;
111
+
112
+ -- AlterTable (contract: the three tables whose column is NOT NULL)
113
+ ALTER TABLE "storage_objects" ALTER COLUMN "org_id" SET NOT NULL;
114
+ ALTER TABLE "storage_object_chunks" ALTER COLUMN "org_id" SET NOT NULL;
115
+ ALTER TABLE "ai_runs" ALTER COLUMN "org_id" SET NOT NULL;
116
+
117
+ -- CreateIndex
118
+ CREATE UNIQUE INDEX "storage_objects_id_org_id_key" ON "storage_objects"("id", "org_id");
119
+
120
+ -- CreateIndex
121
+ CREATE INDEX "storage_objects_org_id_uploaded_by_id_created_at_idx" ON "storage_objects"("org_id", "uploaded_by_id", "created_at");
122
+
123
+ -- CreateIndex
124
+ CREATE INDEX "storage_objects_org_id_status_idx" ON "storage_objects"("org_id", "status");
125
+
126
+ -- CreateIndex
127
+ CREATE INDEX "storage_object_chunks_org_id_object_id_idx" ON "storage_object_chunks"("org_id", "object_id");
128
+
129
+ -- CreateIndex
130
+ CREATE INDEX "ai_runs_org_id_user_id_created_at_idx" ON "ai_runs"("org_id", "user_id", "created_at");
131
+
132
+ -- CreateIndex
133
+ CREATE INDEX "ai_usage_events_org_id_user_id_created_at_idx" ON "ai_usage_events"("org_id", "user_id", "created_at");
134
+
135
+ -- CreateIndex
136
+ CREATE INDEX "ai_usage_events_org_id_created_at_idx" ON "ai_usage_events"("org_id", "created_at");
137
+
138
+ -- CreateIndex
139
+ CREATE INDEX "audit_events_org_id_created_at_idx" ON "audit_events"("org_id", "created_at");
140
+
141
+ -- AddForeignKey
142
+ ALTER TABLE "storage_objects" ADD CONSTRAINT "storage_objects_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
143
+
144
+ -- AddForeignKey (composite: object_id and org_id together)
145
+ ALTER TABLE "storage_object_chunks" ADD CONSTRAINT "storage_object_chunks_object_id_org_id_fkey" FOREIGN KEY ("object_id", "org_id") REFERENCES "storage_objects"("id", "org_id") ON DELETE CASCADE ON UPDATE CASCADE;
146
+
147
+ -- AddForeignKey
148
+ ALTER TABLE "storage_object_chunks" ADD CONSTRAINT "storage_object_chunks_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
149
+
150
+ -- AddForeignKey
151
+ ALTER TABLE "ai_runs" ADD CONSTRAINT "ai_runs_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
152
+
153
+ -- AddForeignKey
154
+ ALTER TABLE "ai_usage_events" ADD CONSTRAINT "ai_usage_events_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
155
+
156
+ -- AddForeignKey
157
+ ALTER TABLE "audit_events" ADD CONSTRAINT "audit_events_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
158
+
159
+ -- =============================================================================
160
+ -- Row-level security. Prisma's schema language cannot express any of this and
161
+ -- its diff ignores it; the policy names are listed in RLS_POLICIES
162
+ -- (packages/platform-db/src/drift/rls-policies.ts) and asserted against
163
+ -- pg_policies by the drift test.
164
+ --
165
+ -- NULLIF(..., '') is required: current_setting(name, true) is NULL on a fresh
166
+ -- connection but the EMPTY STRING on a connection that has ever held a
167
+ -- transaction-local value, and a bare ''::uuid cast errors.
168
+ -- FORCE applies the policy to the table owner, which is the application role:
169
+ -- unscoped code sees no rows and cannot insert (it fails closed).
170
+ -- =============================================================================
171
+
172
+ ALTER TABLE "storage_objects" ENABLE ROW LEVEL SECURITY;
173
+ ALTER TABLE "storage_objects" FORCE ROW LEVEL SECURITY;
174
+ CREATE POLICY "storage_objects_org_isolation" ON "storage_objects"
175
+ USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
176
+ OR current_setting('app.rls_bypass', true) = 'on')
177
+ WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
178
+ OR current_setting('app.rls_bypass', true) = 'on');
179
+
180
+ ALTER TABLE "storage_object_chunks" ENABLE ROW LEVEL SECURITY;
181
+ ALTER TABLE "storage_object_chunks" FORCE ROW LEVEL SECURITY;
182
+ CREATE POLICY "storage_object_chunks_org_isolation" ON "storage_object_chunks"
183
+ USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
184
+ OR current_setting('app.rls_bypass', true) = 'on')
185
+ WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
186
+ OR current_setting('app.rls_bypass', true) = 'on');
187
+
188
+ ALTER TABLE "ai_runs" ENABLE ROW LEVEL SECURITY;
189
+ ALTER TABLE "ai_runs" FORCE ROW LEVEL SECURITY;
190
+ CREATE POLICY "ai_runs_org_isolation" ON "ai_runs"
191
+ USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
192
+ OR current_setting('app.rls_bypass', true) = 'on')
193
+ WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
194
+ OR current_setting('app.rls_bypass', true) = 'on');
195
+
196
+ ALTER TABLE "ai_usage_events" ENABLE ROW LEVEL SECURITY;
197
+ ALTER TABLE "ai_usage_events" FORCE ROW LEVEL SECURITY;
198
+ CREATE POLICY "ai_usage_events_org_isolation" ON "ai_usage_events"
199
+ USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
200
+ OR current_setting('app.rls_bypass', true) = 'on')
201
+ WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
202
+ OR current_setting('app.rls_bypass', true) = 'on');
203
+
204
+ -- A policy is inert for a superuser or a BYPASSRLS role. Warn (do not fail:
205
+ -- the migration role may legitimately differ from the application role).
206
+ DO $$
207
+ DECLARE
208
+ inert boolean;
209
+ BEGIN
210
+ SELECT rolsuper OR rolbypassrls INTO inert FROM pg_roles WHERE rolname = current_user;
211
+ IF inert THEN
212
+ RAISE WARNING 'row-level security is INERT for role "%": it is a superuser or has BYPASSRLS. Run the application as an ordinary role (NOSUPERUSER NOBYPASSRLS) or tenant isolation is not enforced.', current_user;
213
+ END IF;
214
+ END
215
+ $$;