20260802_430_unified_work_center.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """Add the unified governance approval, task and notification work center."""
  2. from alembic import op
  3. revision = "20260802_430"
  4. down_revision = "20260801_420"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.governance_workflows (
  11. uid UUID PRIMARY KEY,
  12. code VARCHAR(120) NOT NULL UNIQUE,
  13. name VARCHAR(300) NOT NULL,
  14. status VARCHAR(20) NOT NULL CHECK (
  15. status IN ('draft','published','retired')
  16. ),
  17. current_version INTEGER NOT NULL CHECK (current_version > 0),
  18. active_version_uid UUID,
  19. created_by UUID NOT NULL REFERENCES public.users(id),
  20. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  21. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  22. );
  23. CREATE TABLE public.governance_workflow_versions (
  24. uid UUID PRIMARY KEY,
  25. workflow_uid UUID NOT NULL REFERENCES public.governance_workflows(uid),
  26. version INTEGER NOT NULL CHECK (version > 0),
  27. status VARCHAR(20) NOT NULL CHECK (
  28. status IN ('draft','published','superseded')
  29. ),
  30. definition JSONB NOT NULL,
  31. created_by UUID NOT NULL REFERENCES public.users(id),
  32. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  33. published_by UUID REFERENCES public.users(id),
  34. published_at TIMESTAMPTZ,
  35. UNIQUE (workflow_uid, version),
  36. CHECK (jsonb_typeof(definition) = 'object')
  37. );
  38. ALTER TABLE public.governance_workflows
  39. ADD CONSTRAINT governance_workflow_active_version_fk
  40. FOREIGN KEY (active_version_uid)
  41. REFERENCES public.governance_workflow_versions(uid);
  42. CREATE UNIQUE INDEX uq_governance_workflow_published_version
  43. ON public.governance_workflow_versions(workflow_uid)
  44. WHERE status = 'published';
  45. CREATE TABLE public.governance_tasks (
  46. uid UUID PRIMARY KEY,
  47. task_code VARCHAR(40) NOT NULL UNIQUE,
  48. workflow_uid UUID NOT NULL REFERENCES public.governance_workflows(uid),
  49. workflow_version INTEGER NOT NULL CHECK (workflow_version > 0),
  50. task_type VARCHAR(40) NOT NULL CHECK (
  51. task_type IN (
  52. 'approval','quality_issue','semantic_governance',
  53. 'data_product_approval','agent_approval',
  54. 'governance_work_order','release','high_risk'
  55. )
  56. ),
  57. subject_type VARCHAR(40) NOT NULL CHECK (
  58. subject_type IN (
  59. 'quality_issue','semantic_governance','data_product','agent'
  60. )
  61. ),
  62. subject_uid VARCHAR(200) NOT NULL,
  63. source_type VARCHAR(80) NOT NULL,
  64. source_uid VARCHAR(200) NOT NULL,
  65. title VARCHAR(300) NOT NULL,
  66. description VARCHAR(2000) NOT NULL,
  67. business_domain_uid VARCHAR(200),
  68. priority VARCHAR(20) NOT NULL CHECK (
  69. priority IN ('low','medium','high','critical')
  70. ),
  71. status VARCHAR(30) NOT NULL CHECK (
  72. status IN (
  73. 'pending','in_progress','pending_review','approved',
  74. 'rejected','closed','reopened','cancelled'
  75. )
  76. ),
  77. assignee_uid UUID NOT NULL REFERENCES public.users(id),
  78. due_at TIMESTAMPTZ NOT NULL,
  79. escalation_level INTEGER NOT NULL DEFAULT 0 CHECK (
  80. escalation_level BETWEEN 0 AND 10
  81. ),
  82. current_version INTEGER NOT NULL DEFAULT 1 CHECK (
  83. current_version > 0
  84. ),
  85. route_snapshot JSONB NOT NULL,
  86. context JSONB NOT NULL DEFAULT '{}'::jsonb,
  87. source_state_unchanged BOOLEAN NOT NULL DEFAULT TRUE CHECK (
  88. source_state_unchanged
  89. ),
  90. created_by UUID NOT NULL REFERENCES public.users(id),
  91. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  92. updated_by UUID NOT NULL REFERENCES public.users(id),
  93. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  94. closed_at TIMESTAMPTZ,
  95. CHECK (jsonb_typeof(route_snapshot) = 'object'),
  96. CHECK (jsonb_typeof(context) = 'object')
  97. );
  98. CREATE UNIQUE INDEX uq_governance_task_active_source
  99. ON public.governance_tasks(source_type, source_uid)
  100. WHERE status NOT IN ('closed','cancelled');
  101. CREATE INDEX idx_governance_task_worklist
  102. ON public.governance_tasks(
  103. assignee_uid, status, priority, due_at, created_at DESC
  104. );
  105. CREATE INDEX idx_governance_task_domain
  106. ON public.governance_tasks(
  107. business_domain_uid, status, due_at
  108. );
  109. CREATE TABLE public.governance_task_participants (
  110. uid UUID PRIMARY KEY,
  111. task_uid UUID NOT NULL
  112. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  113. user_uid UUID NOT NULL REFERENCES public.users(id),
  114. sequence INTEGER NOT NULL CHECK (sequence > 0),
  115. status VARCHAR(20) NOT NULL CHECK (
  116. status IN ('pending','decided','transferred')
  117. ),
  118. transferred_from_uid UUID REFERENCES public.users(id),
  119. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  120. UNIQUE (task_uid, user_uid)
  121. );
  122. CREATE INDEX idx_governance_task_participant_user
  123. ON public.governance_task_participants(user_uid, status, task_uid);
  124. CREATE TABLE public.governance_task_reviews (
  125. uid UUID PRIMARY KEY,
  126. task_uid UUID NOT NULL
  127. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  128. reviewer_uid UUID NOT NULL REFERENCES public.users(id),
  129. decision VARCHAR(20) NOT NULL CHECK (
  130. decision IN ('approve','reject')
  131. ),
  132. reason VARCHAR(1000) NOT NULL,
  133. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  134. UNIQUE (task_uid, reviewer_uid)
  135. );
  136. CREATE TABLE public.governance_task_comments (
  137. uid UUID PRIMARY KEY,
  138. task_uid UUID NOT NULL
  139. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  140. content VARCHAR(4000) NOT NULL,
  141. mentions JSONB NOT NULL DEFAULT '[]'::jsonb,
  142. created_by UUID NOT NULL REFERENCES public.users(id),
  143. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  144. CHECK (jsonb_typeof(mentions) = 'array')
  145. );
  146. CREATE TABLE public.governance_task_attachments (
  147. uid UUID PRIMARY KEY,
  148. task_uid UUID NOT NULL
  149. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  150. name VARCHAR(300) NOT NULL,
  151. storage_ref VARCHAR(1000) NOT NULL,
  152. content_hash CHAR(64) NOT NULL,
  153. created_by UUID NOT NULL REFERENCES public.users(id),
  154. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  155. UNIQUE (task_uid, content_hash)
  156. );
  157. CREATE TABLE public.governance_task_events (
  158. uid UUID PRIMARY KEY,
  159. task_uid UUID NOT NULL
  160. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  161. task_version INTEGER NOT NULL CHECK (task_version > 0),
  162. action VARCHAR(40) NOT NULL,
  163. actor_uid UUID NOT NULL REFERENCES public.users(id),
  164. before_state JSONB NOT NULL DEFAULT '{}'::jsonb,
  165. after_state JSONB NOT NULL,
  166. payload JSONB NOT NULL DEFAULT '{}'::jsonb,
  167. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  168. UNIQUE (task_uid, task_version, action),
  169. CHECK (jsonb_typeof(before_state) = 'object'),
  170. CHECK (jsonb_typeof(after_state) = 'object'),
  171. CHECK (jsonb_typeof(payload) = 'object')
  172. );
  173. CREATE INDEX idx_governance_task_event_timeline
  174. ON public.governance_task_events(task_uid, created_at, uid);
  175. CREATE TABLE public.governance_notification_templates (
  176. uid UUID PRIMARY KEY,
  177. code VARCHAR(120) NOT NULL,
  178. channel VARCHAR(20) NOT NULL CHECK (
  179. channel IN ('in_app','email')
  180. ),
  181. subject_template VARCHAR(500) NOT NULL,
  182. body_template VARCHAR(4000) NOT NULL,
  183. status VARCHAR(20) NOT NULL CHECK (
  184. status IN ('active','retired')
  185. ),
  186. current_version INTEGER NOT NULL DEFAULT 1 CHECK (
  187. current_version > 0
  188. ),
  189. created_by UUID NOT NULL REFERENCES public.users(id),
  190. updated_by UUID NOT NULL REFERENCES public.users(id),
  191. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  192. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  193. UNIQUE (code, channel)
  194. );
  195. CREATE TABLE public.governance_notification_preferences (
  196. user_uid UUID PRIMARY KEY REFERENCES public.users(id),
  197. enabled_channels JSONB NOT NULL DEFAULT '["in_app","email"]'::jsonb,
  198. subscribed_events JSONB NOT NULL DEFAULT '[]'::jsonb,
  199. quiet_hours JSONB NOT NULL DEFAULT '{}'::jsonb,
  200. revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
  201. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  202. CHECK (jsonb_typeof(enabled_channels) = 'array'),
  203. CHECK (jsonb_typeof(subscribed_events) = 'array'),
  204. CHECK (jsonb_typeof(quiet_hours) = 'object')
  205. );
  206. CREATE TABLE public.governance_notifications (
  207. uid UUID PRIMARY KEY,
  208. event_key VARCHAR(500) NOT NULL UNIQUE,
  209. event_type VARCHAR(80) NOT NULL,
  210. recipient_uid UUID NOT NULL REFERENCES public.users(id),
  211. channel VARCHAR(20) NOT NULL CHECK (
  212. channel IN ('in_app','email')
  213. ),
  214. subject VARCHAR(500) NOT NULL,
  215. body VARCHAR(4000) NOT NULL,
  216. related_task_uid UUID
  217. REFERENCES public.governance_tasks(uid) ON DELETE CASCADE,
  218. status VARCHAR(20) NOT NULL CHECK (
  219. status IN (
  220. 'pending','processing','delivered','suppressed','dead_letter'
  221. )
  222. ),
  223. attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
  224. max_attempts INTEGER NOT NULL DEFAULT 5 CHECK (max_attempts > 0),
  225. next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  226. last_error VARCHAR(1000),
  227. delivered_at TIMESTAMPTZ,
  228. read_at TIMESTAMPTZ,
  229. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  230. );
  231. CREATE INDEX idx_governance_notification_inbox
  232. ON public.governance_notifications(
  233. recipient_uid, channel, read_at, created_at DESC
  234. );
  235. CREATE INDEX idx_governance_notification_delivery
  236. ON public.governance_notifications(
  237. channel, status, next_attempt_at
  238. );
  239. CREATE TABLE public.governance_notification_attempts (
  240. uid UUID PRIMARY KEY,
  241. notification_uid UUID NOT NULL
  242. REFERENCES public.governance_notifications(uid) ON DELETE CASCADE,
  243. attempt INTEGER NOT NULL CHECK (attempt > 0),
  244. status VARCHAR(20) NOT NULL CHECK (
  245. status IN ('pending','delivered','dead_letter')
  246. ),
  247. safe_error VARCHAR(1000),
  248. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  249. UNIQUE (notification_uid, attempt)
  250. );
  251. """
  252. )
  253. def downgrade() -> None:
  254. raise RuntimeError(
  255. "workflow, task and notification evidence is retained; "
  256. "downgrade requires an approved archival migration"
  257. )