20260723_200_rule_publication_gates.py 10 KB

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