Files
content-factory/apps/backend/src/application/seed_data.py
T

300 lines
11 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from hashlib import sha256
from uuid import NAMESPACE_URL, UUID, uuid5
from src.application.demo_runtime import (
ensure_demo_bare_repository,
is_demo_mode,
resolve_demo_repository_path,
)
from src.domain.contracts import (
PublishingRules,
Role,
ScriptConfigVersionStatus,
WorkflowTemplateStatus,
)
from src.domain.schema import (
SEEDED_ADMIN_EMAIL,
SEEDED_EDITOR_EMAIL,
SEEDED_TARGET_SITE_SLUG,
)
SEED_TIMESTAMP = datetime(2026, 1, 1, tzinfo=UTC)
SEEDED_WORKFLOW_TEMPLATE_SLUG = "demo-article-production"
def seed_reference_data(repository: object) -> None:
admin_id = _stable_uuid("user:admin")
editor_id = _stable_uuid("user:editor")
target_site_id = _stable_uuid(f"target-site:{SEEDED_TARGET_SITE_SLUG}")
script_config_version_id = _stable_uuid(
f"script-config-version:{SEEDED_TARGET_SITE_SLUG}:1"
)
repository.users.upsert(
user_id=admin_id,
email=SEEDED_ADMIN_EMAIL,
display_name="Admin User",
role=Role.ADMIN,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
)
repository.users.upsert(
user_id=editor_id,
email=SEEDED_EDITOR_EMAIL,
display_name="Editor User",
role=Role.EDITOR,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
)
repository_url = _repository_url_for_seed()
publishing_rules = PublishingRules(
repository_url=repository_url,
production_branch="main",
content_format="mdx",
content_path_template="content/articles/{slug}.mdx",
asset_path_template="public/articles/{slug}/{filename}",
frontmatter_mapping={
"title": "title",
"description": "meta_description",
"publishedAt": "published_at",
},
transform_script_version_id=script_config_version_id,
dry_run_renderer="next-mdx",
)
repository.target_sites.upsert(
site_id=target_site_id,
name="B2B SaaS Blog",
slug=SEEDED_TARGET_SITE_SLUG,
publishing_type="git_next",
default_language="en",
brand_voice="Clear, practical, evidence-led B2B editorial voice.",
audience="B2B SaaS founders and growth teams.",
seo_rules={
"title_max_length": 60,
"meta_description_max_length": 155,
"primary_keyword_required": True,
},
visual_rules={
"hero_style": "product editorial",
"diagram_format": "mermaid",
},
source_rules={
"minimum_sources": 3,
"preferred_source_types": ["primary", "analyst", "vendor_docs"],
},
publishing_rules=publishing_rules,
active_script_config_version_id=script_config_version_id,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
)
publishing_yaml = f"""target: b2b_saas_blog
repository:
url: {repository_url}
branch: main
content:
format: mdx
path_template: content/articles/{{slug}}.mdx
assets:
path_template: public/articles/{{slug}}/{{filename}}
"""
transform_script = """export function transformArticle(article) {
return {
frontmatter: article.frontmatter,
body: article.body,
assets: article.assets,
};
}
"""
repository.script_config_versions.upsert(
version_id=script_config_version_id,
target_site_id=target_site_id,
version=1,
status=ScriptConfigVersionStatus.ACTIVE,
created_by=admin_id,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
diff={"summary": "Initial Git-backed Next.js publishing configuration."},
rollback_target_version_id=None,
activated_at=SEED_TIMESTAMP,
publishing_yaml=publishing_yaml,
publishing_yaml_hash=_sha256(publishing_yaml),
transform_script=transform_script,
transform_script_hash=_sha256(transform_script),
)
_seed_demo_workflow_template(repository, admin_id=admin_id)
def _seed_demo_workflow_template(repository: object, *, admin_id: UUID) -> None:
existing = next(
(
workflow
for workflow in repository.workflow_templates.list()
if workflow.slug == SEEDED_WORKFLOW_TEMPLATE_SLUG
),
None,
)
if existing is not None:
return
workflow = repository.workflow_templates.create(
name="Demo article production workflow",
slug=SEEDED_WORKFLOW_TEMPLATE_SLUG,
description="Active demo workflow for article intake, drafting, review, and publishing.",
status=WorkflowTemplateStatus.DRAFT,
version=1,
created_by=admin_id,
updated_by=admin_id,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
)
for stage in _demo_workflow_stages():
repository.workflow_template_stages.create(
workflow_id=workflow.id,
stable_key=stage["stable_key"],
display_name=stage["display_name"],
description=stage["description"],
position=stage["position"],
owner_role=stage["owner_role"],
runner_profile_key=stage["runner_profile_key"],
required_inputs=stage["required_inputs"],
expected_outputs=stage["expected_outputs"],
acceptance_criteria=stage["acceptance_criteria"],
requires_human_approval=stage["requires_human_approval"],
retry_policy=stage["retry_policy"],
parts=stage["parts"],
updated_by=admin_id,
created_at=SEED_TIMESTAMP,
updated_at=SEED_TIMESTAMP,
)
repository.workflow_templates.activate(
workflow_id=workflow.id,
updated_by=admin_id,
activated_at=SEED_TIMESTAMP,
)
def _demo_workflow_stages() -> list[dict[str, object]]:
return [
{
"stable_key": "intake-boundary-questions",
"display_name": "Boundary question intake",
"description": "Collect required editorial context before plan generation.",
"position": 1,
"owner_role": Role.EDITOR,
"runner_profile_key": "boundary-question-agent-v1",
"required_inputs": ["article_brief", "target_site"],
"expected_outputs": ["answered_boundary_questions"],
"acceptance_criteria": ["All required boundary questions are answered."],
"requires_human_approval": True,
"retry_policy": {"max_attempts": 1, "backoff_seconds": 0},
"parts": [],
},
{
"stable_key": "plan-review",
"display_name": "Plan review",
"description": "Generate and approve the article plan before research.",
"position": 2,
"owner_role": Role.EDITOR,
"runner_profile_key": "plan-generation-agent-v1",
"required_inputs": ["answered_boundary_questions"],
"expected_outputs": ["approved_plan"],
"acceptance_criteria": ["Plan is approved by the editor."],
"requires_human_approval": True,
"retry_policy": {"max_attempts": 2, "backoff_seconds": 30},
"parts": [],
},
{
"stable_key": "research-evidence",
"display_name": "Research and evidence",
"description": "Run research and approve evidence before production starts.",
"position": 3,
"owner_role": Role.EDITOR,
"runner_profile_key": "research-agent-v1",
"required_inputs": ["approved_plan"],
"expected_outputs": ["approved_evidence_matrix"],
"acceptance_criteria": ["Evidence matrix is ready and approved."],
"requires_human_approval": True,
"retry_policy": {"max_attempts": 2, "backoff_seconds": 60},
"parts": [],
},
{
"stable_key": "parallel-production",
"display_name": "Parallel production",
"description": "Create section scaffolds and draft-ready assets in parallel.",
"position": 4,
"owner_role": Role.EDITOR,
"runner_profile_key": "section-scaffold-agent-v1",
"required_inputs": ["approved_evidence_matrix"],
"expected_outputs": ["section_scaffolds", "asset_specs"],
"acceptance_criteria": ["All production jobs have succeeded or been retried."],
"requires_human_approval": False,
"retry_policy": {"max_attempts": 2, "backoff_seconds": 60},
"parts": [],
},
{
"stable_key": "draft-assembly",
"display_name": "Draft assembly",
"description": "Assemble a draft from the approved plan and production artifacts.",
"position": 5,
"owner_role": Role.EDITOR,
"runner_profile_key": "draft-assembly-agent-v1",
"required_inputs": ["section_scaffolds", "approved_assets"],
"expected_outputs": ["article_draft"],
"acceptance_criteria": ["Draft is assembled for review."],
"requires_human_approval": False,
"retry_policy": {"max_attempts": 2, "backoff_seconds": 60},
"parts": [],
},
{
"stable_key": "seo-language-review",
"display_name": "SEO and language review",
"description": "Run automated quality reviews and resolve required suggestions.",
"position": 6,
"owner_role": Role.EDITOR,
"runner_profile_key": "review-agent-v1",
"required_inputs": ["article_draft"],
"expected_outputs": ["resolved_review_suggestions"],
"acceptance_criteria": ["Required SEO and language suggestions are resolved."],
"requires_human_approval": True,
"retry_policy": {"max_attempts": 2, "backoff_seconds": 60},
"parts": [],
},
{
"stable_key": "final-publishing",
"display_name": "Final approval and publishing",
"description": "Approve final draft, dry-run the bundle, and create the publish commit.",
"position": 7,
"owner_role": Role.EDITOR,
"runner_profile_key": "publishing-agent-v1",
"required_inputs": ["resolved_review_suggestions", "approved_assets"],
"expected_outputs": ["publish_commit"],
"acceptance_criteria": ["Publish dry-run passes and commit is created."],
"requires_human_approval": True,
"retry_policy": {"max_attempts": 1, "backoff_seconds": 0},
"parts": [],
},
]
def _repository_url_for_seed() -> str:
if not is_demo_mode():
return "git@github.com:example/site.git"
demo_repo_path = resolve_demo_repository_path()
ensure_demo_bare_repository(demo_repo_path)
return str(demo_repo_path)
def _stable_uuid(value: str) -> UUID:
return uuid5(NAMESPACE_URL, f"ai-content-pipeline:{value}")
def _sha256(value: str) -> str:
return sha256(value.encode("utf-8")).hexdigest()