feat(task-013): add draft assembly API, immutable versioning, and editor preview
This commit is contained in:
@@ -18,6 +18,8 @@ from src.domain.contracts import (
|
||||
ClaimRiskLevel,
|
||||
ClaimSupportStatus,
|
||||
ClaimSummary,
|
||||
DraftFaqItem,
|
||||
DraftSummary,
|
||||
EvidenceSummary,
|
||||
PlanReviewStatus,
|
||||
PlanSectionSummary,
|
||||
@@ -52,6 +54,7 @@ class BackendRepository:
|
||||
self.articles = ArticlesRepository(self)
|
||||
self.boundary_questions = BoundaryQuestionsRepository(self)
|
||||
self.article_plans = ArticlePlansRepository(self)
|
||||
self.article_drafts = ArticleDraftsRepository(self)
|
||||
self.research_manifests = ResearchManifestsRepository(self)
|
||||
self.evidence_items = EvidenceItemsRepository(self)
|
||||
self.claims = ClaimsRepository(self)
|
||||
@@ -1014,6 +1017,181 @@ class ArticlePlansRepository:
|
||||
"""
|
||||
|
||||
|
||||
class ArticleDraftsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def create_version(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
version: int,
|
||||
title: str,
|
||||
slug: str,
|
||||
meta_title: str | None,
|
||||
meta_description: str | None,
|
||||
body_object_key: str | None,
|
||||
body_markdown: str,
|
||||
faq_items: list[JsonObject],
|
||||
visual_placeholders: list[str],
|
||||
evidence_references: list[UUID],
|
||||
unsupported_claim_warnings: list[str],
|
||||
based_on_draft_id: UUID | None,
|
||||
status: ArticleWorkflowStatus,
|
||||
created_at: datetime,
|
||||
updated_at: datetime,
|
||||
) -> DraftSummary:
|
||||
draft_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO article_drafts (
|
||||
id,
|
||||
article_id,
|
||||
version,
|
||||
title,
|
||||
slug,
|
||||
meta_title,
|
||||
meta_description,
|
||||
body_object_key,
|
||||
body_markdown,
|
||||
faq_items,
|
||||
visual_placeholders,
|
||||
evidence_references,
|
||||
unsupported_claim_warnings,
|
||||
based_on_draft_id,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(draft_id),
|
||||
str(article_id),
|
||||
version,
|
||||
title,
|
||||
slug,
|
||||
meta_title,
|
||||
meta_description,
|
||||
body_object_key,
|
||||
body_markdown,
|
||||
_json_value(faq_items),
|
||||
_json_value(visual_placeholders),
|
||||
_json_value([str(item_id) for item_id in evidence_references]),
|
||||
_json_value(unsupported_claim_warnings),
|
||||
_uuid_value(based_on_draft_id),
|
||||
status.value,
|
||||
_datetime_value(created_at),
|
||||
_datetime_value(updated_at),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(article_id=article_id, draft_id=draft_id)
|
||||
|
||||
def list_for_article(self, article_id: UUID) -> list[DraftSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM article_drafts
|
||||
WHERE article_id = {placeholder}
|
||||
ORDER BY version DESC
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchall()
|
||||
|
||||
return [_draft_summary_from_row(row) for row in rows]
|
||||
|
||||
def get(self, *, article_id: UUID, draft_id: UUID) -> DraftSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM article_drafts
|
||||
WHERE article_id = {placeholder} AND id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(draft_id)),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
raise LookupError(f"Draft not found: {draft_id}")
|
||||
return _draft_summary_from_row(row)
|
||||
|
||||
def latest_for_article(self, article_id: UUID) -> DraftSummary | None:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM article_drafts
|
||||
WHERE article_id = {placeholder}
|
||||
ORDER BY version DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return _draft_summary_from_row(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_drafts
|
||||
WHERE article_id = {placeholder}
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchone()
|
||||
return int(_row_value(row, "version"))
|
||||
|
||||
def _select_columns(self) -> str:
|
||||
return """
|
||||
id,
|
||||
article_id,
|
||||
version,
|
||||
title,
|
||||
slug,
|
||||
meta_title,
|
||||
meta_description,
|
||||
body_object_key,
|
||||
body_markdown,
|
||||
faq_items,
|
||||
visual_placeholders,
|
||||
evidence_references,
|
||||
unsupported_claim_warnings,
|
||||
based_on_draft_id,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
"""
|
||||
|
||||
|
||||
class ResearchManifestsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
@@ -2176,6 +2354,35 @@ def _claim_from_row(row: Any) -> ClaimSummary:
|
||||
)
|
||||
|
||||
|
||||
def _draft_summary_from_row(row: Any) -> DraftSummary:
|
||||
return DraftSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
version=_row_value(row, "version"),
|
||||
title=_row_value(row, "title"),
|
||||
slug=_row_value(row, "slug"),
|
||||
meta_title=_row_value(row, "meta_title"),
|
||||
meta_description=_row_value(row, "meta_description"),
|
||||
body_object_key=_row_value(row, "body_object_key"),
|
||||
body_markdown=_row_value(row, "body_markdown"),
|
||||
faq_items=[
|
||||
DraftFaqItem.model_validate(item)
|
||||
for item in _json_from_row(row, "faq_items")
|
||||
],
|
||||
visual_placeholders=_json_from_row(row, "visual_placeholders"),
|
||||
evidence_references=[
|
||||
UUID(value) for value in _json_from_row(row, "evidence_references")
|
||||
],
|
||||
unsupported_claim_warnings=_json_from_row(
|
||||
row, "unsupported_claim_warnings"
|
||||
),
|
||||
based_on_draft_id=_row_value(row, "based_on_draft_id"),
|
||||
status=_row_value(row, "status"),
|
||||
created_at=_row_value(row, "created_at"),
|
||||
updated_at=_row_value(row, "updated_at"),
|
||||
)
|
||||
|
||||
|
||||
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
|
||||
return AgentJobSummary(
|
||||
id=_row_value(row, "id"),
|
||||
|
||||
Reference in New Issue
Block a user