Task 009 implement plan generation review gate

This commit is contained in:
2026-05-21 19:56:58 +03:00
parent b7e2fa34b2
commit 5b27d1a3ca
24 changed files with 2422 additions and 59 deletions
@@ -15,6 +15,9 @@ from src.domain.contracts import (
AgentJobType,
ArticleSummary,
BoundaryQuestionSummary,
PlanReviewStatus,
PlanSectionSummary,
PlanSummary,
PublishingRules,
PublishingStatus,
ArticleWorkflowStatus,
@@ -42,6 +45,7 @@ class BackendRepository:
self.target_sites = TargetSitesRepository(self)
self.articles = ArticlesRepository(self)
self.boundary_questions = BoundaryQuestionsRepository(self)
self.article_plans = ArticlePlansRepository(self)
self.agent_jobs = AgentJobsRepository(self)
self.script_config_versions = ScriptConfigVersionsRepository(self)
self.script_config_version_events = ScriptConfigVersionAuditEventsRepository(self)
@@ -750,6 +754,257 @@ class BoundaryQuestionsRepository:
return self.get(article_id=article_id, question_id=question_id)
class ArticlePlansRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
def create_version(
self,
*,
article_id: UUID,
version: int,
status: PlanReviewStatus,
title_options: list[str],
recommended_title: str | None,
reader_persona: str | None,
search_intent: str | None,
thesis: str | None,
claims_to_prove: list[str],
evidence_needs: list[str],
visual_needs: list[str],
seo_notes: list[str],
source_requirements: list[str],
excluded_sources: list[str],
tone: str | None,
audience: str | None,
risks: list[str],
sections: list[JsonObject],
created_at: datetime,
) -> PlanSummary:
plan_id = uuid4()
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO article_plans (
id,
article_id,
version,
status,
title_options,
recommended_title,
reader_persona,
search_intent,
thesis,
claims_to_prove,
evidence_needs,
visual_needs,
seo_notes,
source_requirements,
excluded_sources,
tone,
audience,
risks,
created_at
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder}
)
""",
(
str(plan_id),
str(article_id),
version,
status.value,
_json_value(title_options),
recommended_title,
reader_persona,
search_intent,
thesis,
_json_value(claims_to_prove),
_json_value(evidence_needs),
_json_value(visual_needs),
_json_value(seo_notes),
_json_value(source_requirements),
_json_value(excluded_sources),
tone,
audience,
_json_value(risks),
_datetime_value(created_at),
),
)
for index, section in enumerate(sections, start=1):
connection.execute(
f"""
INSERT INTO plan_sections (
id,
article_plan_id,
sort_order,
heading,
purpose,
key_points,
evidence_needs,
claims_to_support,
target_word_count
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}
)
""",
(
str(uuid4()),
str(plan_id),
index,
section["heading"],
section.get("purpose"),
_json_value(section.get("key_points", [])),
_json_value(section.get("evidence_needs", [])),
_json_value(section.get("claims_to_support", [])),
section.get("target_word_count"),
),
)
return self.get(article_id=article_id, plan_id=plan_id)
def list_for_article(self, article_id: UUID) -> list[PlanSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT {self._select_columns()}
FROM article_plans
WHERE article_id = {placeholder}
ORDER BY version
""",
(str(article_id),),
).fetchall()
return [self._with_sections(row) for row in rows]
def get(self, *, article_id: UUID, plan_id: UUID) -> PlanSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT {self._select_columns()}
FROM article_plans
WHERE article_id = {placeholder} AND id = {placeholder}
""",
(str(article_id), str(plan_id)),
).fetchone()
if row is None:
raise LookupError(f"Plan not found: {plan_id}")
return self._with_sections(row)
def latest_version(self, article_id: UUID) -> int:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT COALESCE(MAX(version), 0) AS version
FROM article_plans
WHERE article_id = {placeholder}
""",
(str(article_id),),
).fetchone()
return int(_row_value(row, "version"))
def update_status(
self,
*,
article_id: UUID,
plan_id: UUID,
status: PlanReviewStatus,
) -> PlanSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
connection.execute(
f"""
UPDATE article_plans
SET status = {placeholder}
WHERE article_id = {placeholder} AND id = {placeholder}
""",
(status.value, str(article_id), str(plan_id)),
)
return self.get(article_id=article_id, plan_id=plan_id)
def _with_sections(self, row: Any) -> PlanSummary:
plan_id = _row_value(row, "id")
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
section_rows = connection.execute(
f"""
SELECT
id,
article_plan_id,
sort_order,
heading,
purpose,
key_points,
evidence_needs,
claims_to_support,
target_word_count
FROM plan_sections
WHERE article_plan_id = {placeholder}
ORDER BY sort_order
""",
(str(plan_id),),
).fetchall()
return _plan_summary_from_row(row, [_plan_section_from_row(item) for item in section_rows])
def _select_columns(self) -> str:
return """
id,
article_id,
version,
status,
title_options,
recommended_title,
reader_persona,
search_intent,
thesis,
claims_to_prove,
evidence_needs,
visual_needs,
seo_notes,
source_requirements,
excluded_sources,
tone,
audience,
risks,
created_at
"""
class AgentJobsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
@@ -1418,6 +1673,48 @@ def _boundary_question_from_row(row: Any) -> BoundaryQuestionSummary:
)
def _plan_section_from_row(row: Any) -> PlanSectionSummary:
return PlanSectionSummary(
id=_row_value(row, "id"),
article_plan_id=_row_value(row, "article_plan_id"),
sort_order=_row_value(row, "sort_order"),
heading=_row_value(row, "heading"),
purpose=_row_value(row, "purpose"),
key_points=_json_from_row(row, "key_points"),
evidence_needs=_json_from_row(row, "evidence_needs"),
claims_to_support=_json_from_row(row, "claims_to_support"),
target_word_count=_row_value(row, "target_word_count"),
)
def _plan_summary_from_row(
row: Any,
sections: list[PlanSectionSummary],
) -> PlanSummary:
return PlanSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
version=_row_value(row, "version"),
status=_row_value(row, "status"),
title_options=_json_from_row(row, "title_options"),
recommended_title=_row_value(row, "recommended_title"),
reader_persona=_row_value(row, "reader_persona"),
search_intent=_row_value(row, "search_intent"),
thesis=_row_value(row, "thesis"),
sections=sections,
claims_to_prove=_json_from_row(row, "claims_to_prove"),
evidence_needs=_json_from_row(row, "evidence_needs"),
visual_needs=_json_from_row(row, "visual_needs"),
seo_notes=_json_from_row(row, "seo_notes"),
source_requirements=_json_from_row(row, "source_requirements"),
excluded_sources=_json_from_row(row, "excluded_sources"),
tone=_row_value(row, "tone"),
audience=_row_value(row, "audience"),
risks=_json_from_row(row, "risks"),
created_at=_row_value(row, "created_at"),
)
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
return AgentJobSummary(
id=_row_value(row, "id"),