694 lines
26 KiB
Python
694 lines
26 KiB
Python
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,
|
|
)
|
|
|
|
|
|
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
|
|
)
|
|
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 articles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
target_site_id UUID NOT NULL REFERENCES target_sites(id),
|
|
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()
|
|
)
|
|
""",
|
|
"""
|
|
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,
|
|
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)
|
|
)
|
|
""",
|
|
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,
|
|
asset_type TEXT NOT NULL CHECK (asset_type IN ({ASSET_TYPE_VALUES})),
|
|
title TEXT NOT NULL,
|
|
prompt 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()
|
|
)
|
|
""",
|
|
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,
|
|
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",
|
|
"""
|
|
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_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_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_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)
|
|
""",
|
|
)
|
|
|
|
|
|
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 articles (
|
|
id TEXT PRIMARY KEY,
|
|
target_site_id TEXT NOT NULL,
|
|
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,
|
|
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 assets (
|
|
id TEXT PRIMARY KEY,
|
|
article_id TEXT NOT NULL,
|
|
asset_type TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
prompt 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 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 '[]',
|
|
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_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_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_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)
|
|
""",
|
|
)
|
|
|
|
|
|
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)
|