20260723_200_rule_publication_gates.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 = 'published'
  15. ) OR EXISTS (
  16. SELECT 1 FROM public.rule_execution_plans
  17. WHERE status = 'published'
  18. ) THEN
  19. RAISE EXCEPTION
  20. 'legacy published rules or plans cannot be trusted by '
  21. 'migration 200; revoke or rebuild them through the '
  22. 'Task7 compile-test-publication chain';
  23. END IF;
  24. IF EXISTS (
  25. SELECT 1 FROM public.data_rule_versions
  26. WHERE status NOT IN ('draft','validated','published')
  27. ) THEN
  28. RAISE EXCEPTION
  29. 'legacy rule version statuses must be resolved before 200';
  30. END IF;
  31. END $$;
  32. ALTER TABLE public.data_rule_versions
  33. DROP CONSTRAINT data_rule_versions_status_check;
  34. ALTER TABLE public.data_rule_versions
  35. ADD CONSTRAINT data_rule_versions_status_check
  36. CHECK (status IN ('draft','validated','published','revoked'));
  37. ALTER TABLE public.rule_execution_plans
  38. DROP CONSTRAINT rule_execution_plans_status_check;
  39. ALTER TABLE public.rule_execution_plans
  40. ADD CONSTRAINT rule_execution_plans_status_check
  41. CHECK (status IN ('compiled','tested','published','revoked'));
  42. ALTER TABLE public.rule_generation_runs
  43. ADD COLUMN receipt_hash CHAR(64),
  44. ADD COLUMN receipt_consumed_at TIMESTAMPTZ,
  45. ADD COLUMN validation_context JSONB,
  46. ADD COLUMN model_hash CHAR(64),
  47. ADD COLUMN prompt_hash CHAR(64);
  48. CREATE UNIQUE INDEX uq_rule_generation_linked_version
  49. ON public.rule_generation_runs(rule_version_id)
  50. WHERE rule_version_id IS NOT NULL;
  51. CREATE UNIQUE INDEX uq_rule_generation_receipt_hash
  52. ON public.rule_generation_runs(receipt_hash)
  53. WHERE receipt_hash IS NOT NULL;
  54. CREATE TABLE public.rule_generation_attempts (
  55. id UUID PRIMARY KEY,
  56. generation_run_id UUID NOT NULL
  57. REFERENCES public.rule_generation_runs(id) ON DELETE RESTRICT,
  58. attempt_no INTEGER NOT NULL CHECK (attempt_no BETWEEN 0 AND 2),
  59. candidate_hash CHAR(64) NOT NULL,
  60. candidate JSONB,
  61. error_code VARCHAR(80),
  62. model_hash CHAR(64) NOT NULL,
  63. prompt_hash CHAR(64) NOT NULL,
  64. context_hash CHAR(64) NOT NULL,
  65. status VARCHAR(20) NOT NULL CHECK (status IN ('valid','invalid')),
  66. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  67. UNIQUE (generation_run_id, attempt_no)
  68. );
  69. CREATE TABLE public.rule_validation_profiles (
  70. id UUID PRIMARY KEY,
  71. rule_version_id UUID NOT NULL UNIQUE
  72. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  73. input_schema_snapshot_id UUID NOT NULL
  74. REFERENCES public.data_schema_snapshots(id) ON DELETE RESTRICT,
  75. input_schema_hash CHAR(64) NOT NULL,
  76. input_fields JSONB NOT NULL,
  77. output_schema_snapshot_id UUID NOT NULL
  78. REFERENCES public.data_schema_snapshots(id) ON DELETE RESTRICT,
  79. output_schema_hash CHAR(64) NOT NULL,
  80. output_fields JSONB NOT NULL,
  81. input_sample_artifact_ref VARCHAR(500) NOT NULL,
  82. input_sample_artifact_digest CHAR(64) NOT NULL,
  83. golden_output_artifact_ref VARCHAR(500),
  84. golden_output_artifact_digest CHAR(64),
  85. context_hash CHAR(64) NOT NULL,
  86. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  87. CHECK (
  88. (golden_output_artifact_ref IS NULL
  89. AND golden_output_artifact_digest IS NULL)
  90. OR
  91. (golden_output_artifact_ref IS NOT NULL
  92. AND golden_output_artifact_digest IS NOT NULL)
  93. )
  94. );
  95. CREATE TABLE public.rule_logical_plans (
  96. id UUID PRIMARY KEY,
  97. rule_version_id UUID NOT NULL
  98. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  99. validation_profile_id UUID NOT NULL
  100. REFERENCES public.rule_validation_profiles(id)
  101. ON DELETE RESTRICT,
  102. compiler_version VARCHAR(80) NOT NULL,
  103. backend VARCHAR(30) NOT NULL CHECK (
  104. backend IN ('sql_pushdown','polars_batch')
  105. ),
  106. plan JSONB NOT NULL,
  107. plan_hash CHAR(64) NOT NULL,
  108. schema_hashes JSONB NOT NULL,
  109. capabilities JSONB NOT NULL,
  110. status VARCHAR(20) NOT NULL CHECK (
  111. status IN ('compiled','tested','published','revoked')
  112. ),
  113. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  114. UNIQUE (rule_version_id, plan_hash)
  115. );
  116. CREATE TABLE public.rule_logical_compile_evidence (
  117. id UUID PRIMARY KEY,
  118. logical_plan_id UUID NOT NULL
  119. REFERENCES public.rule_logical_plans(id) ON DELETE RESTRICT,
  120. compiler_version VARCHAR(80) NOT NULL,
  121. compiler_digest CHAR(64) NOT NULL,
  122. plan_hash CHAR(64) NOT NULL,
  123. schema_hashes JSONB NOT NULL,
  124. capabilities JSONB NOT NULL,
  125. status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed')),
  126. created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
  127. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  128. UNIQUE (logical_plan_id, compiler_digest)
  129. );
  130. CREATE TABLE public.rule_logical_test_evidence (
  131. id UUID PRIMARY KEY,
  132. logical_plan_id UUID NOT NULL
  133. REFERENCES public.rule_logical_plans(id) ON DELETE RESTRICT,
  134. test_kind VARCHAR(40) NOT NULL,
  135. evidence_hash CHAR(64) NOT NULL,
  136. run_id UUID NOT NULL,
  137. plan_hash CHAR(64) NOT NULL,
  138. schema_hashes JSONB NOT NULL,
  139. evidence JSONB NOT NULL,
  140. status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed')),
  141. created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
  142. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  143. UNIQUE (logical_plan_id, test_kind, evidence_hash)
  144. );
  145. ALTER TABLE public.rule_compile_evidence
  146. ADD COLUMN plan_hash CHAR(64),
  147. ADD COLUMN schema_hashes JSONB,
  148. ADD COLUMN binding_hashes JSONB,
  149. ADD COLUMN capabilities JSONB,
  150. ADD COLUMN legacy_untrusted BOOLEAN NOT NULL DEFAULT TRUE,
  151. ADD COLUMN created_by UUID REFERENCES public.users(id)
  152. ON DELETE SET NULL;
  153. UPDATE public.rule_compile_evidence e
  154. SET plan_hash = p.plan_hash,
  155. schema_hashes = p.schema_hashes,
  156. binding_hashes = jsonb_build_object(
  157. 'input', COALESCE(p.plan->>'input_binding_hash', ''),
  158. 'output', COALESCE(p.plan->>'output_binding_hash', '')
  159. ),
  160. capabilities = COALESCE(
  161. p.plan->'capabilities',
  162. jsonb_build_object('backend', p.backend)
  163. )
  164. FROM public.rule_execution_plans p
  165. WHERE p.id = e.rule_execution_plan_id;
  166. ALTER TABLE public.rule_compile_evidence
  167. ALTER COLUMN plan_hash SET NOT NULL,
  168. ALTER COLUMN schema_hashes SET NOT NULL,
  169. ALTER COLUMN binding_hashes SET NOT NULL,
  170. ALTER COLUMN capabilities SET NOT NULL;
  171. ALTER TABLE public.rule_test_evidence
  172. ADD COLUMN plan_hash CHAR(64),
  173. ADD COLUMN schema_hashes JSONB,
  174. ADD COLUMN binding_hashes JSONB,
  175. ADD COLUMN run_id UUID,
  176. ADD COLUMN legacy_untrusted BOOLEAN NOT NULL DEFAULT TRUE,
  177. ADD COLUMN created_by UUID REFERENCES public.users(id)
  178. ON DELETE SET NULL;
  179. UPDATE public.rule_test_evidence e
  180. SET plan_hash = p.plan_hash,
  181. schema_hashes = p.schema_hashes,
  182. binding_hashes = jsonb_build_object(
  183. 'input', COALESCE(p.plan->>'input_binding_hash', ''),
  184. 'output', COALESCE(p.plan->>'output_binding_hash', '')
  185. ),
  186. run_id = e.id
  187. FROM public.rule_execution_plans p
  188. WHERE p.id = e.rule_execution_plan_id;
  189. ALTER TABLE public.rule_test_evidence
  190. ALTER COLUMN plan_hash SET NOT NULL,
  191. ALTER COLUMN schema_hashes SET NOT NULL,
  192. ALTER COLUMN binding_hashes SET NOT NULL,
  193. ALTER COLUMN run_id SET NOT NULL;
  194. CREATE TABLE public.rule_validation_attempts (
  195. id UUID PRIMARY KEY,
  196. rule_version_id UUID NOT NULL
  197. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  198. actor_uid UUID REFERENCES public.users(id) ON DELETE SET NULL,
  199. error_code VARCHAR(80) NOT NULL,
  200. error_hash CHAR(64) NOT NULL,
  201. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  202. );
  203. CREATE TABLE public.rule_publication_audits (
  204. id UUID PRIMARY KEY,
  205. rule_version_id UUID NOT NULL
  206. REFERENCES public.data_rule_versions(id) ON DELETE RESTRICT,
  207. rule_execution_plan_id UUID
  208. REFERENCES public.rule_execution_plans(id) ON DELETE RESTRICT,
  209. actor_uid UUID REFERENCES public.users(id) ON DELETE SET NULL,
  210. action VARCHAR(30) NOT NULL CHECK (
  211. action IN (
  212. 'draft_created','compile_succeeded','test_succeeded',
  213. 'published','revoked'
  214. )
  215. ),
  216. from_status VARCHAR(20),
  217. to_status VARCHAR(20) NOT NULL,
  218. evidence_hash CHAR(64),
  219. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  220. );
  221. CREATE INDEX idx_rule_publication_audit_version
  222. ON public.rule_publication_audits(
  223. rule_version_id, created_at DESC
  224. );
  225. CREATE INDEX idx_published_rule_catalog
  226. ON public.data_rule_versions(status, published_at DESC)
  227. WHERE status = 'published';
  228. """
  229. )
  230. def downgrade() -> None:
  231. raise RuntimeError(
  232. "trusted rule publication evidence is forward-only and cannot be "
  233. "downgraded without invalidating signed receipts and audit history"
  234. )