300 lines
11 KiB
Python
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()
|