20260723_200_rule_publication_gates.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. """Add trusted generation receipts and compile/test publication gates."""
  2. from alembic import op
  3. revision = "20260723_200"
  4. down_revision = "20260723_190"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. DO $$
  11. BEGIN
  12. IF EXISTS (
  13. SELECT 1 FROM public.data_rule_versions
  14. WHERE status NOT IN ('draft','validated','published')
  15. ) THEN
  16. RAISE EXCEPTION
  17. 'legacy rule version statuses must be resolved before 200';
  18. END IF;
  19. END $$;
  20. ALTER TABLE public.data_rule_versions
  21. DROP CONSTRAINT data_rule_versions_status_check;
  22. ALTER TABLE public.data_rule_versions
  23. ADD CONSTRAINT data_rule_versions_status_check
  24. CHECK (status IN ('draft','validated','published','revoked'));
  25. ALTER TABLE public.rule_execution_plans
  26. DROP CONSTRAINT rule_execution_plans_status_check;
  27. ALTER TABLE public.rule_execution_plans
  28. ADD CONSTRAINT rule_execution_plans_status_check
  29. CHECK (status IN ('compiled','tested','published','revoked'));
  30. ALTER TABLE public.rule_generation_runs
  31. ADD COLUMN receipt_hash CHAR(64),
  32. ADD COLUMN receipt_consumed_at TIMESTAMPTZ,
  33. ADD COLUMN validation_context JSONB,
  34. ADD COLUMN model_hash CHAR(64),
  35. ADD COLUMN prompt_hash CHAR(64);
  36. CREATE UNIQUE INDEX uq_rule_generation_linked_version
  37. ON public.rule_generation_runs(rule_version_id)
  38. WHERE rule_version_id IS NOT NULL;
  39. CREATE UNIQUE INDEX uq_rule_generation_receipt_hash
  40. ON public.rule_generation_runs(receipt_hash)
  41. WHERE receipt_hash IS NOT NULL;
  42. CREATE TABLE public.rule_generation_attempts (
  43. id UUID PRIMARY KEY,
  44. generation_run_id UUID NOT NULL
  45. REFERENCES public.rule_generation_runs(id) ON DELETE RESTRICT,
  46. attempt_no INTEGER NOT NULL CHECK (attempt_no BETWEEN 0 AND 2),
  47. candidate_hash CHAR(64) NOT NULL,
  48. candidate JSONB,
  49. error_code VARCHAR(80),
  50. model_hash CHAR(64) NOT NULL,
  51. prompt_hash CHAR(64) NOT NULL,
  52. context_hash CHAR(64) NOT NULL,
  53. status VARCHAR(20) NOT NULL CHECK (status IN ('valid','invalid')),
  54. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  55. UNIQUE (generation_run_id, attempt_no)
  56. );
  57. CREATE TABLE public.rule_validation_profiles (
  58. id UUID PRIMARY KEY,
  59. rule_version_id UUID NOT NULL UNIQUE
  60. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  61. schema_snapshot_id UUID NOT NULL
  62. REFERENCES public.data_schema_snapshots(id) ON DELETE RESTRICT,
  63. schema_hash CHAR(64) NOT NULL,
  64. sample_artifact_ref VARCHAR(500),
  65. context_hash CHAR(64) NOT NULL,
  66. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  67. );
  68. CREATE TABLE public.rule_logical_plans (
  69. id UUID PRIMARY KEY,
  70. rule_version_id UUID NOT NULL
  71. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  72. validation_profile_id UUID NOT NULL
  73. REFERENCES public.rule_validation_profiles(id)
  74. ON DELETE RESTRICT,
  75. compiler_version VARCHAR(80) NOT NULL,
  76. backend VARCHAR(30) NOT NULL CHECK (
  77. backend IN ('sql_pushdown','polars_batch')
  78. ),
  79. plan JSONB NOT NULL,
  80. plan_hash CHAR(64) NOT NULL,
  81. schema_hashes JSONB NOT NULL,
  82. capabilities JSONB NOT NULL,
  83. status VARCHAR(20) NOT NULL CHECK (
  84. status IN ('compiled','tested','published','revoked')
  85. ),
  86. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  87. UNIQUE (rule_version_id, plan_hash)
  88. );
  89. CREATE TABLE public.rule_logical_compile_evidence (
  90. id UUID PRIMARY KEY,
  91. logical_plan_id UUID NOT NULL
  92. REFERENCES public.rule_logical_plans(id) ON DELETE RESTRICT,
  93. compiler_version VARCHAR(80) NOT NULL,
  94. compiler_digest CHAR(64) NOT NULL,
  95. plan_hash CHAR(64) NOT NULL,
  96. schema_hashes JSONB NOT NULL,
  97. capabilities JSONB NOT NULL,
  98. status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed')),
  99. created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
  100. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  101. UNIQUE (logical_plan_id, compiler_digest)
  102. );
  103. CREATE TABLE public.rule_logical_test_evidence (
  104. id UUID PRIMARY KEY,
  105. logical_plan_id UUID NOT NULL
  106. REFERENCES public.rule_logical_plans(id) ON DELETE RESTRICT,
  107. test_kind VARCHAR(40) NOT NULL,
  108. evidence_hash CHAR(64) NOT NULL,
  109. run_id UUID NOT NULL,
  110. plan_hash CHAR(64) NOT NULL,
  111. schema_hashes JSONB NOT NULL,
  112. evidence JSONB NOT NULL,
  113. status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed')),
  114. created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
  115. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  116. UNIQUE (logical_plan_id, test_kind, evidence_hash)
  117. );
  118. ALTER TABLE public.rule_compile_evidence
  119. ADD COLUMN plan_hash CHAR(64),
  120. ADD COLUMN schema_hashes JSONB,
  121. ADD COLUMN binding_hashes JSONB,
  122. ADD COLUMN capabilities JSONB,
  123. ADD COLUMN created_by UUID REFERENCES public.users(id)
  124. ON DELETE SET NULL;
  125. UPDATE public.rule_compile_evidence e
  126. SET plan_hash = p.plan_hash,
  127. schema_hashes = p.schema_hashes,
  128. binding_hashes = jsonb_build_object(
  129. 'input', COALESCE(p.plan->>'input_binding_hash', ''),
  130. 'output', COALESCE(p.plan->>'output_binding_hash', '')
  131. ),
  132. capabilities = COALESCE(
  133. p.plan->'capabilities',
  134. jsonb_build_object('backend', p.backend)
  135. )
  136. FROM public.rule_execution_plans p
  137. WHERE p.id = e.rule_execution_plan_id;
  138. ALTER TABLE public.rule_compile_evidence
  139. ALTER COLUMN plan_hash SET NOT NULL,
  140. ALTER COLUMN schema_hashes SET NOT NULL,
  141. ALTER COLUMN binding_hashes SET NOT NULL,
  142. ALTER COLUMN capabilities SET NOT NULL;
  143. ALTER TABLE public.rule_test_evidence
  144. ADD COLUMN plan_hash CHAR(64),
  145. ADD COLUMN schema_hashes JSONB,
  146. ADD COLUMN binding_hashes JSONB,
  147. ADD COLUMN run_id UUID,
  148. ADD COLUMN created_by UUID REFERENCES public.users(id)
  149. ON DELETE SET NULL;
  150. UPDATE public.rule_test_evidence e
  151. SET plan_hash = p.plan_hash,
  152. schema_hashes = p.schema_hashes,
  153. binding_hashes = jsonb_build_object(
  154. 'input', COALESCE(p.plan->>'input_binding_hash', ''),
  155. 'output', COALESCE(p.plan->>'output_binding_hash', '')
  156. ),
  157. run_id = e.id
  158. FROM public.rule_execution_plans p
  159. WHERE p.id = e.rule_execution_plan_id;
  160. ALTER TABLE public.rule_test_evidence
  161. ALTER COLUMN plan_hash SET NOT NULL,
  162. ALTER COLUMN schema_hashes SET NOT NULL,
  163. ALTER COLUMN binding_hashes SET NOT NULL,
  164. ALTER COLUMN run_id SET NOT NULL;
  165. CREATE TABLE public.rule_validation_attempts (
  166. id UUID PRIMARY KEY,
  167. rule_version_id UUID NOT NULL
  168. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  169. actor_uid UUID REFERENCES public.users(id) ON DELETE SET NULL,
  170. error_code VARCHAR(80) NOT NULL,
  171. error_hash CHAR(64) NOT NULL,
  172. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  173. );
  174. CREATE TABLE public.rule_publication_audits (
  175. id UUID PRIMARY KEY,
  176. rule_version_id UUID NOT NULL
  177. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  178. rule_execution_plan_id UUID
  179. REFERENCES public.rule_execution_plans(id) ON DELETE RESTRICT,
  180. actor_uid UUID REFERENCES public.users(id) ON DELETE SET NULL,
  181. action VARCHAR(30) NOT NULL CHECK (
  182. action IN (
  183. 'draft_created','compile_succeeded','test_succeeded',
  184. 'published','revoked'
  185. )
  186. ),
  187. from_status VARCHAR(20),
  188. to_status VARCHAR(20) NOT NULL,
  189. evidence_hash CHAR(64),
  190. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  191. );
  192. CREATE INDEX idx_rule_publication_audit_version
  193. ON public.rule_publication_audits(
  194. rule_version_id, created_at DESC
  195. );
  196. CREATE INDEX idx_published_rule_catalog
  197. ON public.data_rule_versions(status, published_at DESC)
  198. WHERE status = 'published';
  199. """
  200. )
  201. def downgrade() -> None:
  202. raise RuntimeError(
  203. "trusted rule publication evidence is forward-only and cannot be "
  204. "downgraded without invalidating signed receipts and audit history"
  205. )