from __future__ import annotations import sqlite3 from collections.abc import Iterable from contextlib import closing from src.domain.contracts import ( AgentJobErrorCategory, AgentJobStatus, AgentJobType, ArticleWorkflowStatus, AssetStatus, AssetType, ClaimRiskLevel, ClaimSupportStatus, PlanReviewStatus, PublishingStatus, Role, ScriptConfigVersionStatus, WorkflowTemplateStatus, ) def _quoted_values(values: Iterable[str]) -> str: return ", ".join(f"'{value}'" for value in values) ROLE_VALUES = _quoted_values(role.value for role in Role) ARTICLE_STATUS_VALUES = _quoted_values(status.value for status in ArticleWorkflowStatus) PUBLISHING_STATUS_VALUES = _quoted_values(status.value for status in PublishingStatus) SCRIPT_CONFIG_STATUS_VALUES = _quoted_values( status.value for status in ScriptConfigVersionStatus ) WORKFLOW_TEMPLATE_STATUS_VALUES = _quoted_values( status.value for status in WorkflowTemplateStatus ) PLAN_REVIEW_STATUS_VALUES = _quoted_values(status.value for status in PlanReviewStatus) CLAIM_SUPPORT_STATUS_VALUES = _quoted_values( status.value for status in ClaimSupportStatus ) CLAIM_RISK_LEVEL_VALUES = _quoted_values(level.value for level in ClaimRiskLevel) ASSET_TYPE_VALUES = _quoted_values(asset_type.value for asset_type in AssetType) ASSET_STATUS_VALUES = _quoted_values(status.value for status in AssetStatus) AGENT_JOB_TYPE_VALUES = _quoted_values(job_type.value for job_type in AgentJobType) AGENT_JOB_STATUS_VALUES = _quoted_values(status.value for status in AgentJobStatus) AGENT_JOB_ERROR_CATEGORY_VALUES = _quoted_values( category.value for category in AgentJobErrorCategory ) POSTGRES_SCHEMA_STATEMENTS: tuple[str, ...] = ( 'CREATE EXTENSION IF NOT EXISTS "pgcrypto"', f""" CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ({ROLE_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, """ CREATE TABLE IF NOT EXISTS target_sites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, publishing_type TEXT NOT NULL DEFAULT 'git_next', default_language TEXT NOT NULL DEFAULT 'en', brand_voice TEXT NOT NULL, audience TEXT NOT NULL, seo_rules JSONB NOT NULL DEFAULT '{}'::jsonb, visual_rules JSONB NOT NULL DEFAULT '{}'::jsonb, source_rules JSONB NOT NULL DEFAULT '{}'::jsonb, publishing_rules JSONB NOT NULL DEFAULT '{}'::jsonb, active_script_config_version_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS script_config_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id) ON DELETE CASCADE, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({SCRIPT_CONFIG_STATUS_VALUES})), created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), diff JSONB NOT NULL DEFAULT '{{}}'::jsonb, rollback_target_version_id UUID REFERENCES script_config_versions(id), activated_at TIMESTAMPTZ, publishing_yaml TEXT NOT NULL, publishing_yaml_hash TEXT NOT NULL, transform_script TEXT NOT NULL, transform_script_hash TEXT NOT NULL, UNIQUE (target_site_id, version) ) """, f""" CREATE TABLE IF NOT EXISTS workflow_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ({WORKFLOW_TEMPLATE_STATUS_VALUES})), version INTEGER NOT NULL, created_by UUID NOT NULL REFERENCES users(id), updated_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), activated_at TIMESTAMPTZ, archived_at TIMESTAMPTZ ) """, """ CREATE TABLE IF NOT EXISTS workflow_template_stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workflow_template_id UUID NOT NULL REFERENCES workflow_templates(id) ON DELETE CASCADE, stable_key TEXT NOT NULL, display_name TEXT NOT NULL, description TEXT NOT NULL, position INTEGER NOT NULL, owner_role TEXT NOT NULL CHECK (owner_role IN ('ADMIN', 'EDITOR')), runner_profile_key TEXT NOT NULL, required_inputs JSONB NOT NULL DEFAULT '[]'::jsonb, expected_outputs JSONB NOT NULL DEFAULT '[]'::jsonb, acceptance_criteria JSONB NOT NULL DEFAULT '[]'::jsonb, requires_human_approval BOOLEAN NOT NULL DEFAULT false, retry_policy JSONB NOT NULL DEFAULT '{}'::jsonb, parts JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (workflow_template_id, stable_key) ) """, f""" CREATE TABLE IF NOT EXISTS workflow_template_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workflow_template_id UUID NOT NULL REFERENCES workflow_templates(id) ON DELETE CASCADE, event_type TEXT NOT NULL, actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS articles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id), workflow_template_id UUID REFERENCES workflow_templates(id), workflow_template_version INTEGER, workflow_template_snapshot JSONB, status TEXT NOT NULL CHECK (status IN ({ARTICLE_STATUS_VALUES})), publishing_status TEXT NOT NULL CHECK ( publishing_status IN ({PUBLISHING_STATUS_VALUES}) ), brief_description TEXT NOT NULL, working_title TEXT, language TEXT NOT NULL DEFAULT 'en', content_type TEXT NOT NULL DEFAULT 'longform_guide', primary_keyword TEXT, assigned_editor_id UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "ALTER TABLE articles ADD COLUMN IF NOT EXISTS workflow_template_id UUID REFERENCES workflow_templates(id)", "ALTER TABLE articles ADD COLUMN IF NOT EXISTS workflow_template_version INTEGER", "ALTER TABLE articles ADD COLUMN IF NOT EXISTS workflow_template_snapshot JSONB", """ CREATE TABLE IF NOT EXISTS boundary_questions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL, category TEXT NOT NULL DEFAULT 'general', question TEXT NOT NULL, answer TEXT, is_required BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, sort_order) ) """, "ALTER TABLE boundary_questions ADD COLUMN IF NOT EXISTS category TEXT NOT NULL DEFAULT 'general'", f""" CREATE TABLE IF NOT EXISTS article_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({PLAN_REVIEW_STATUS_VALUES})), title_options JSONB NOT NULL DEFAULT '[]'::jsonb, recommended_title TEXT, reader_persona TEXT, search_intent TEXT, thesis TEXT, claims_to_prove JSONB NOT NULL DEFAULT '[]'::jsonb, evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb, visual_needs JSONB NOT NULL DEFAULT '[]'::jsonb, seo_notes JSONB NOT NULL DEFAULT '[]'::jsonb, source_requirements JSONB NOT NULL DEFAULT '[]'::jsonb, excluded_sources JSONB NOT NULL DEFAULT '[]'::jsonb, tone TEXT, audience TEXT, risks JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, version) ) """, "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS title_options JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS reader_persona TEXT", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS visual_needs JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS seo_notes JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS source_requirements JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS excluded_sources JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS tone TEXT", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS audience TEXT", """ CREATE TABLE IF NOT EXISTS plan_sections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_plan_id UUID NOT NULL REFERENCES article_plans(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL, heading TEXT NOT NULL, purpose TEXT, key_points JSONB NOT NULL DEFAULT '[]'::jsonb, evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb, claims_to_support JSONB NOT NULL DEFAULT '[]'::jsonb, target_word_count INTEGER, UNIQUE (article_plan_id, sort_order) ) """, "ALTER TABLE plan_sections ADD COLUMN IF NOT EXISTS key_points JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE plan_sections ADD COLUMN IF NOT EXISTS evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb", """ CREATE TABLE IF NOT EXISTS evidence_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, source_title TEXT NOT NULL, source_url TEXT NOT NULL, source_type TEXT NOT NULL, source_quality_score NUMERIC(4, 3) NOT NULL, summary TEXT NOT NULL, supports_claims JSONB NOT NULL DEFAULT '[]'::jsonb, artifact_manifest_id UUID, retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now(), review_status TEXT NOT NULL DEFAULT 'PENDING' ) """, "ALTER TABLE evidence_items ADD COLUMN IF NOT EXISTS review_status TEXT NOT NULL DEFAULT 'PENDING'", f""" CREATE TABLE IF NOT EXISTS claims ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, section_id UUID REFERENCES plan_sections(id) ON DELETE SET NULL, claim_text TEXT NOT NULL, support_status TEXT NOT NULL CHECK ( support_status IN ({CLAIM_SUPPORT_STATUS_VALUES}) ), risk_level TEXT NOT NULL CHECK (risk_level IN ({CLAIM_RISK_LEVEL_VALUES})), evidence_item_ids JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS article_drafts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, version INTEGER NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, meta_title TEXT, meta_description TEXT, body_object_key TEXT, body_markdown TEXT NOT NULL DEFAULT '', faq_items JSONB NOT NULL DEFAULT '[]'::jsonb, visual_placeholders JSONB NOT NULL DEFAULT '[]'::jsonb, evidence_references JSONB NOT NULL DEFAULT '[]'::jsonb, unsupported_claim_warnings JSONB NOT NULL DEFAULT '[]'::jsonb, based_on_draft_id UUID REFERENCES article_drafts(id) ON DELETE SET NULL, status TEXT NOT NULL CHECK (status IN ({ARTICLE_STATUS_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, version) ) """, "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS body_markdown TEXT NOT NULL DEFAULT ''", "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS faq_items JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS visual_placeholders JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS evidence_references JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS unsupported_claim_warnings JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_drafts ADD COLUMN IF NOT EXISTS based_on_draft_id UUID REFERENCES article_drafts(id) ON DELETE SET NULL", """ CREATE TABLE IF NOT EXISTS article_review_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, review_kind TEXT NOT NULL CHECK (review_kind IN ('SEO', 'LANGUAGE')), draft_id UUID NOT NULL REFERENCES article_drafts(id) ON DELETE CASCADE, score INTEGER NOT NULL, issues JSONB NOT NULL DEFAULT '[]'::jsonb, recommended_slug TEXT NOT NULL, recommended_title TEXT NOT NULL, schema_json JSONB NOT NULL DEFAULT '{}'::jsonb, rules_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, """ CREATE TABLE IF NOT EXISTS article_review_suggestions ( id UUID PRIMARY KEY, article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, review_kind TEXT NOT NULL CHECK (review_kind IN ('SEO', 'LANGUAGE')), report_id UUID NOT NULL REFERENCES article_review_reports(id) ON DELETE CASCADE, suggestion_key TEXT NOT NULL, severity TEXT NOT NULL, location TEXT NOT NULL, message TEXT NOT NULL, suggested_fix TEXT, suggested_rewrite TEXT, patch JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL CHECK (status IN ('PENDING', 'ACCEPTED', 'REJECTED', 'EDITED')), applied_text TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, review_kind, suggestion_key) ) """, f""" CREATE TABLE IF NOT EXISTS assets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, section_id UUID REFERENCES plan_sections(id) ON DELETE SET NULL, asset_type TEXT NOT NULL CHECK (asset_type IN ({ASSET_TYPE_VALUES})), title TEXT NOT NULL, prompt TEXT, object_key TEXT, file_url TEXT, alt_text TEXT, caption TEXT, status TEXT NOT NULL CHECK (status IN ({ASSET_STATUS_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "ALTER TABLE assets ADD COLUMN IF NOT EXISTS section_id UUID REFERENCES plan_sections(id) ON DELETE SET NULL", "ALTER TABLE assets ADD COLUMN IF NOT EXISTS object_key TEXT", f""" CREATE TABLE IF NOT EXISTS asset_revisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, asset_id UUID NOT NULL REFERENCES assets(id) ON DELETE CASCADE, revision_index INTEGER NOT NULL, action TEXT NOT NULL, actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS workflow_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, event_type TEXT NOT NULL, from_status TEXT CHECK (from_status IN ({ARTICLE_STATUS_VALUES})), to_status TEXT CHECK (to_status IN ({ARTICLE_STATUS_VALUES})), actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS agent_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID REFERENCES articles(id) ON DELETE SET NULL, parent_job_id UUID REFERENCES agent_jobs(id) ON DELETE SET NULL, attempt INTEGER NOT NULL DEFAULT 1, job_type TEXT NOT NULL CHECK (job_type IN ({AGENT_JOB_TYPE_VALUES})), agent_profile TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ({AGENT_JOB_STATUS_VALUES})), workspace_path TEXT, input_files JSONB NOT NULL DEFAULT '[]'::jsonb, output_files JSONB NOT NULL DEFAULT '[]'::jsonb, payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, error_category TEXT CHECK ( error_category IS NULL OR error_category IN ({AGENT_JOB_ERROR_CATEGORY_VALUES}) ), error_message TEXT, stdout TEXT NOT NULL DEFAULT '', stderr TEXT NOT NULL DEFAULT '', exit_code INTEGER, duration_ms INTEGER, queued_at TIMESTAMPTZ NOT NULL DEFAULT now(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ ) """, "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS parent_job_id UUID REFERENCES agent_jobs(id) ON DELETE SET NULL", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS attempt INTEGER NOT NULL DEFAULT 1", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS stdout TEXT NOT NULL DEFAULT ''", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS stderr TEXT NOT NULL DEFAULT ''", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS exit_code INTEGER", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS duration_ms INTEGER", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS payload JSONB NOT NULL DEFAULT '{}'::jsonb", """ CREATE TABLE IF NOT EXISTS research_run_manifests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, agent_job_id UUID NOT NULL REFERENCES agent_jobs(id) ON DELETE CASCADE, s3_prefix TEXT NOT NULL, source_urls JSONB NOT NULL DEFAULT '[]'::jsonb, object_keys JSONB NOT NULL DEFAULT '[]'::jsonb, content_hashes JSONB NOT NULL DEFAULT '[]'::jsonb, artifact_types JSONB NOT NULL DEFAULT '[]'::jsonb, metadata_references JSONB NOT NULL DEFAULT '[]'::jsonb, artifacts JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS publish_commits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, target_site_id UUID NOT NULL REFERENCES target_sites(id), repository_url TEXT NOT NULL, branch TEXT NOT NULL, commit_sha TEXT, content_bundle_manifest JSONB NOT NULL DEFAULT '{{}}'::jsonb, status TEXT NOT NULL CHECK (status IN ({PUBLISHING_STATUS_VALUES})), deployment_status TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS prompt_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), prompt_key TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({SCRIPT_CONFIG_STATUS_VALUES})), body TEXT NOT NULL, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), activated_at TIMESTAMPTZ, diff JSONB NOT NULL DEFAULT '{{}}'::jsonb, UNIQUE (prompt_key, version) ) """, f""" CREATE TABLE IF NOT EXISTS script_config_version_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id) ON DELETE CASCADE, version_id UUID NOT NULL REFERENCES script_config_versions(id) ON DELETE CASCADE, event_type TEXT NOT NULL, actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS idx_target_sites_slug ON target_sites (slug)", "CREATE INDEX IF NOT EXISTS idx_workflow_templates_slug ON workflow_templates (slug)", """ CREATE INDEX IF NOT EXISTS idx_workflow_templates_status_updated ON workflow_templates (status, updated_at DESC) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_template_stages_order ON workflow_template_stages (workflow_template_id, position) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_template_events_history ON workflow_template_events (workflow_template_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_articles_dashboard ON articles (target_site_id, status, publishing_status, updated_at DESC) """, """ CREATE INDEX IF NOT EXISTS idx_articles_editor_dashboard ON articles (assigned_editor_id, updated_at DESC) """, """ CREATE INDEX IF NOT EXISTS idx_articles_workflow_template ON articles (workflow_template_id, workflow_template_version) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_queue ON agent_jobs (status, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_article_timeline ON agent_jobs (article_id, job_type, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_events_timeline ON workflow_events (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_asset_revisions_asset ON asset_revisions (asset_id, revision_index, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_research_run_manifests_article ON research_run_manifests (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_publish_commits_article ON publish_commits (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_script_config_version_events_site ON script_config_version_events (target_site_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_article_review_reports_article ON article_review_reports (article_id, review_kind, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_article_review_suggestions_report ON article_review_suggestions (report_id, status) """, ) SQLITE_SCHEMA_STATEMENTS: tuple[str, ...] = ( """ CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, role TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS target_sites ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, publishing_type TEXT NOT NULL DEFAULT 'git_next', default_language TEXT NOT NULL DEFAULT 'en', brand_voice TEXT NOT NULL, audience TEXT NOT NULL, seo_rules TEXT NOT NULL DEFAULT '{}', visual_rules TEXT NOT NULL DEFAULT '{}', source_rules TEXT NOT NULL DEFAULT '{}', publishing_rules TEXT NOT NULL DEFAULT '{}', active_script_config_version_id TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS script_config_versions ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, diff TEXT NOT NULL DEFAULT '{}', rollback_target_version_id TEXT, activated_at TEXT, publishing_yaml TEXT NOT NULL, publishing_yaml_hash TEXT NOT NULL, transform_script TEXT NOT NULL, transform_script_hash TEXT NOT NULL, UNIQUE (target_site_id, version) ) """, """ CREATE TABLE IF NOT EXISTS workflow_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT NOT NULL, status TEXT NOT NULL, version INTEGER NOT NULL, created_by TEXT NOT NULL, updated_by TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, activated_at TEXT, archived_at TEXT ) """, """ CREATE TABLE IF NOT EXISTS workflow_template_stages ( id TEXT PRIMARY KEY, workflow_template_id TEXT NOT NULL, stable_key TEXT NOT NULL, display_name TEXT NOT NULL, description TEXT NOT NULL, position INTEGER NOT NULL, owner_role TEXT NOT NULL, runner_profile_key TEXT NOT NULL, required_inputs TEXT NOT NULL DEFAULT '[]', expected_outputs TEXT NOT NULL DEFAULT '[]', acceptance_criteria TEXT NOT NULL DEFAULT '[]', requires_human_approval INTEGER NOT NULL DEFAULT 0, retry_policy TEXT NOT NULL DEFAULT '{}', parts TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (workflow_template_id, stable_key) ) """, """ CREATE TABLE IF NOT EXISTS workflow_template_events ( id TEXT PRIMARY KEY, workflow_template_id TEXT NOT NULL, event_type TEXT NOT NULL, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS articles ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, workflow_template_id TEXT, workflow_template_version INTEGER, workflow_template_snapshot TEXT, status TEXT NOT NULL, publishing_status TEXT NOT NULL, brief_description TEXT NOT NULL, working_title TEXT, language TEXT NOT NULL DEFAULT 'en', content_type TEXT NOT NULL DEFAULT 'longform_guide', primary_keyword TEXT, assigned_editor_id TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS boundary_questions ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, sort_order INTEGER NOT NULL, category TEXT NOT NULL DEFAULT 'general', question TEXT NOT NULL, answer TEXT, is_required INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, sort_order) ) """, """ CREATE TABLE IF NOT EXISTS article_plans ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, title_options TEXT NOT NULL DEFAULT '[]', recommended_title TEXT, reader_persona TEXT, search_intent TEXT, thesis TEXT, claims_to_prove TEXT NOT NULL DEFAULT '[]', evidence_needs TEXT NOT NULL DEFAULT '[]', visual_needs TEXT NOT NULL DEFAULT '[]', seo_notes TEXT NOT NULL DEFAULT '[]', source_requirements TEXT NOT NULL DEFAULT '[]', excluded_sources TEXT NOT NULL DEFAULT '[]', tone TEXT, audience TEXT, risks TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, version) ) """, """ CREATE TABLE IF NOT EXISTS plan_sections ( id TEXT PRIMARY KEY, article_plan_id TEXT NOT NULL, sort_order INTEGER NOT NULL, heading TEXT NOT NULL, purpose TEXT, key_points TEXT NOT NULL DEFAULT '[]', evidence_needs TEXT NOT NULL DEFAULT '[]', claims_to_support TEXT NOT NULL DEFAULT '[]', target_word_count INTEGER, UNIQUE (article_plan_id, sort_order) ) """, """ CREATE TABLE IF NOT EXISTS evidence_items ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, source_title TEXT NOT NULL, source_url TEXT NOT NULL, source_type TEXT NOT NULL, source_quality_score REAL NOT NULL, summary TEXT NOT NULL, supports_claims TEXT NOT NULL DEFAULT '[]', artifact_manifest_id TEXT, retrieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, review_status TEXT NOT NULL DEFAULT 'PENDING' ) """, """ CREATE TABLE IF NOT EXISTS claims ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, section_id TEXT, claim_text TEXT NOT NULL, support_status TEXT NOT NULL, risk_level TEXT NOT NULL, evidence_item_ids TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS article_drafts ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, version INTEGER NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, meta_title TEXT, meta_description TEXT, body_object_key TEXT, body_markdown TEXT NOT NULL DEFAULT '', faq_items TEXT NOT NULL DEFAULT '[]', visual_placeholders TEXT NOT NULL DEFAULT '[]', evidence_references TEXT NOT NULL DEFAULT '[]', unsupported_claim_warnings TEXT NOT NULL DEFAULT '[]', based_on_draft_id TEXT, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, version) ) """, """ CREATE TABLE IF NOT EXISTS article_review_reports ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, review_kind TEXT NOT NULL, draft_id TEXT NOT NULL, score INTEGER NOT NULL, issues TEXT NOT NULL DEFAULT '[]', recommended_slug TEXT NOT NULL, recommended_title TEXT NOT NULL, schema_json TEXT NOT NULL DEFAULT '{}', rules_snapshot TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS article_review_suggestions ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, review_kind TEXT NOT NULL, report_id TEXT NOT NULL, suggestion_key TEXT NOT NULL, severity TEXT NOT NULL, location TEXT NOT NULL, message TEXT NOT NULL, suggested_fix TEXT, suggested_rewrite TEXT, patch TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL, applied_text TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, review_kind, suggestion_key) ) """, """ CREATE TABLE IF NOT EXISTS assets ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, section_id TEXT, asset_type TEXT NOT NULL, title TEXT NOT NULL, prompt TEXT, object_key TEXT, file_url TEXT, alt_text TEXT, caption TEXT, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS asset_revisions ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, asset_id TEXT NOT NULL, revision_index INTEGER NOT NULL, action TEXT NOT NULL, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS workflow_events ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, event_type TEXT NOT NULL, from_status TEXT, to_status TEXT, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS agent_jobs ( id TEXT PRIMARY KEY, article_id TEXT, parent_job_id TEXT, attempt INTEGER NOT NULL DEFAULT 1, job_type TEXT NOT NULL, agent_profile TEXT NOT NULL, status TEXT NOT NULL, workspace_path TEXT, input_files TEXT NOT NULL DEFAULT '[]', output_files TEXT NOT NULL DEFAULT '[]', payload TEXT NOT NULL DEFAULT '{}', error_category TEXT, error_message TEXT, stdout TEXT NOT NULL DEFAULT '', stderr TEXT NOT NULL DEFAULT '', exit_code INTEGER, duration_ms INTEGER, queued_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, started_at TEXT, finished_at TEXT ) """, """ CREATE TABLE IF NOT EXISTS research_run_manifests ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, agent_job_id TEXT NOT NULL, s3_prefix TEXT NOT NULL, source_urls TEXT NOT NULL DEFAULT '[]', object_keys TEXT NOT NULL DEFAULT '[]', content_hashes TEXT NOT NULL DEFAULT '[]', artifact_types TEXT NOT NULL DEFAULT '[]', metadata_references TEXT NOT NULL DEFAULT '[]', artifacts TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS publish_commits ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, target_site_id TEXT NOT NULL, repository_url TEXT NOT NULL, branch TEXT NOT NULL, commit_sha TEXT, content_bundle_manifest TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL, deployment_status TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS prompt_versions ( id TEXT PRIMARY KEY, prompt_key TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, body TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, activated_at TEXT, diff TEXT NOT NULL DEFAULT '{}', UNIQUE (prompt_key, version) ) """, """ CREATE TABLE IF NOT EXISTS script_config_version_events ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, version_id TEXT NOT NULL, event_type TEXT NOT NULL, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, "CREATE INDEX IF NOT EXISTS idx_target_sites_slug ON target_sites (slug)", "CREATE INDEX IF NOT EXISTS idx_workflow_templates_slug ON workflow_templates (slug)", """ CREATE INDEX IF NOT EXISTS idx_workflow_templates_status_updated ON workflow_templates (status, updated_at) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_template_stages_order ON workflow_template_stages (workflow_template_id, position) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_template_events_history ON workflow_template_events (workflow_template_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_articles_dashboard ON articles (target_site_id, status, publishing_status, updated_at) """, """ CREATE INDEX IF NOT EXISTS idx_articles_editor_dashboard ON articles (assigned_editor_id, updated_at) """, """ CREATE INDEX IF NOT EXISTS idx_articles_workflow_template ON articles (workflow_template_id, workflow_template_version) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_queue ON agent_jobs (status, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_article_timeline ON agent_jobs (article_id, job_type, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_events_timeline ON workflow_events (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_asset_revisions_asset ON asset_revisions (asset_id, revision_index, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_research_run_manifests_article ON research_run_manifests (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_publish_commits_article ON publish_commits (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_script_config_version_events_site ON script_config_version_events (target_site_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_article_review_reports_article ON article_review_reports (article_id, review_kind, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_article_review_suggestions_report ON article_review_suggestions (report_id, status) """, ) def schema_statements_for_dsn(dsn: str) -> tuple[str, ...]: if dsn.startswith("sqlite:///"): return SQLITE_SCHEMA_STATEMENTS return POSTGRES_SCHEMA_STATEMENTS def setup_database(dsn: str) -> None: statements = schema_statements_for_dsn(dsn) if dsn.startswith("sqlite:///"): sqlite_path = dsn.removeprefix("sqlite:///") with closing(sqlite3.connect(sqlite_path)) as connection: for statement in statements: connection.execute(statement) connection.commit() return import psycopg with psycopg.connect(dsn) as connection: with connection.cursor() as cursor: for statement in statements: cursor.execute(statement)