task015 implement seo and language review flow

This commit is contained in:
2026-05-22 00:16:08 +03:00
parent 8243f20b0e
commit 6dcbe0e36c
20 changed files with 3828 additions and 14 deletions
@@ -13,6 +13,10 @@ from src.domain.contracts import (
AgentJobStatus,
AgentJobSummary,
AgentJobType,
ContentReviewKind,
ContentReviewIssueSummary,
ContentReviewReportSummary,
ContentReviewSuggestionSummary,
AssetRevisionSummary,
AssetStatus,
AssetSummary,
@@ -33,6 +37,7 @@ from src.domain.contracts import (
ResearchArtifactManifestSummary,
ResearchArtifactSummary,
ArticleWorkflowStatus,
ReviewSuggestionStatus,
WorkflowEventSummary,
Role,
ScriptConfigVersionStatus,
@@ -59,6 +64,7 @@ class BackendRepository:
self.boundary_questions = BoundaryQuestionsRepository(self)
self.article_plans = ArticlePlansRepository(self)
self.article_drafts = ArticleDraftsRepository(self)
self.content_reviews = ContentReviewsRepository(self)
self.assets = AssetsRepository(self)
self.research_manifests = ResearchManifestsRepository(self)
self.evidence_items = EvidenceItemsRepository(self)
@@ -1197,6 +1203,418 @@ class ArticleDraftsRepository:
"""
class ContentReviewsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
def create_report(
self,
*,
article_id: UUID,
review_kind: ContentReviewKind,
draft_id: UUID,
score: int,
recommended_slug: str,
recommended_title: str,
schema_json: JsonObject,
rules_snapshot: JsonObject,
created_at: datetime,
) -> ContentReviewReportSummary:
report_id = uuid4()
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO article_review_reports (
id,
article_id,
review_kind,
draft_id,
score,
issues,
recommended_slug,
recommended_title,
schema_json,
rules_snapshot,
created_at
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder}{json_cast},
{placeholder}
)
""",
(
str(report_id),
str(article_id),
review_kind.value,
str(draft_id),
score,
_json_value([]),
recommended_slug,
recommended_title,
_json_value(schema_json),
_json_value(rules_snapshot),
_datetime_value(created_at),
),
)
return self.get_report(article_id=article_id, report_id=report_id)
def list_reports_for_article(
self,
*,
article_id: UUID,
review_kind: ContentReviewKind,
) -> list[ContentReviewReportSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT
id,
article_id,
review_kind,
draft_id,
score,
recommended_slug,
recommended_title,
schema_json,
rules_snapshot,
created_at
FROM article_review_reports
WHERE article_id = {placeholder}
AND review_kind = {placeholder}
ORDER BY created_at DESC
""",
(str(article_id), review_kind.value),
).fetchall()
return [self.get_report(article_id=article_id, report_id=_row_value(row, "id")) for row in rows]
def latest_report(
self,
*,
article_id: UUID,
review_kind: ContentReviewKind,
) -> ContentReviewReportSummary | None:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT id
FROM article_review_reports
WHERE article_id = {placeholder}
AND review_kind = {placeholder}
ORDER BY created_at DESC
LIMIT 1
""",
(str(article_id), review_kind.value),
).fetchone()
if row is None:
return None
return self.get_report(article_id=article_id, report_id=_row_value(row, "id"))
def get_report(self, *, article_id: UUID, report_id: UUID) -> ContentReviewReportSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT
id,
article_id,
review_kind,
draft_id,
score,
recommended_slug,
recommended_title,
schema_json,
rules_snapshot,
created_at
FROM article_review_reports
WHERE article_id = {placeholder}
AND id = {placeholder}
""",
(str(article_id), str(report_id)),
).fetchone()
if row is None:
raise LookupError(f"Content review report not found: {report_id}")
suggestions = self.list_suggestions_for_report(
article_id=article_id,
report_id=report_id,
)
issues = [
ContentReviewIssueSummary(
id=item.suggestion_key,
suggestion_id=item.id,
severity=item.severity,
location=item.location,
message=item.message,
suggested_fix=item.suggested_fix,
suggested_rewrite=item.suggested_rewrite,
status=item.status,
)
for item in suggestions
]
return _content_review_report_from_row(row, issues=issues)
def upsert_suggestion(
self,
*,
suggestion_id: UUID,
article_id: UUID,
review_kind: ContentReviewKind,
report_id: UUID,
suggestion_key: str,
severity: str,
location: str,
message: str,
suggested_fix: str | None,
suggested_rewrite: str | None,
patch: JsonObject,
created_at: datetime,
updated_at: datetime,
) -> ContentReviewSuggestionSummary:
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO article_review_suggestions (
id,
article_id,
review_kind,
report_id,
suggestion_key,
severity,
location,
message,
suggested_fix,
suggested_rewrite,
patch,
status,
applied_text,
created_at,
updated_at
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder},
{placeholder}
)
ON CONFLICT (article_id, review_kind, suggestion_key) DO UPDATE SET
report_id = excluded.report_id,
severity = excluded.severity,
location = excluded.location,
message = excluded.message,
suggested_fix = excluded.suggested_fix,
suggested_rewrite = excluded.suggested_rewrite,
patch = excluded.patch,
status = CASE
WHEN article_review_suggestions.status = 'PENDING' THEN excluded.status
ELSE article_review_suggestions.status
END,
applied_text = CASE
WHEN article_review_suggestions.status = 'PENDING' THEN excluded.applied_text
ELSE article_review_suggestions.applied_text
END,
updated_at = excluded.updated_at
""",
(
str(suggestion_id),
str(article_id),
review_kind.value,
str(report_id),
suggestion_key,
severity,
location,
message,
suggested_fix,
suggested_rewrite,
_json_value(patch),
ReviewSuggestionStatus.PENDING.value,
None,
_datetime_value(created_at),
_datetime_value(updated_at),
),
)
return self.get_suggestion(
article_id=article_id,
review_kind=review_kind,
suggestion_id=suggestion_id,
)
def get_suggestion(
self,
*,
article_id: UUID,
review_kind: ContentReviewKind,
suggestion_id: UUID,
) -> ContentReviewSuggestionSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT
id,
article_id,
review_kind,
report_id,
suggestion_key,
severity,
location,
message,
suggested_fix,
suggested_rewrite,
patch,
status,
applied_text,
created_at,
updated_at
FROM article_review_suggestions
WHERE article_id = {placeholder}
AND review_kind = {placeholder}
AND id = {placeholder}
""",
(
str(article_id),
review_kind.value,
str(suggestion_id),
),
).fetchone()
if row is None:
raise LookupError(f"Content review suggestion not found: {suggestion_id}")
return _content_review_suggestion_from_row(row)
def list_suggestions_for_report(
self,
*,
article_id: UUID,
report_id: UUID,
) -> list[ContentReviewSuggestionSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT
id,
article_id,
review_kind,
report_id,
suggestion_key,
severity,
location,
message,
suggested_fix,
suggested_rewrite,
patch,
status,
applied_text,
created_at,
updated_at
FROM article_review_suggestions
WHERE article_id = {placeholder}
AND report_id = {placeholder}
ORDER BY suggestion_key
""",
(str(article_id), str(report_id)),
).fetchall()
return [_content_review_suggestion_from_row(row) for row in rows]
def list_unresolved_for_article(
self,
*,
article_id: UUID,
) -> list[ContentReviewSuggestionSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT
id,
article_id,
review_kind,
report_id,
suggestion_key,
severity,
location,
message,
suggested_fix,
suggested_rewrite,
patch,
status,
applied_text,
created_at,
updated_at
FROM article_review_suggestions
WHERE article_id = {placeholder}
AND status = {placeholder}
ORDER BY review_kind, suggestion_key
""",
(str(article_id), ReviewSuggestionStatus.PENDING.value),
).fetchall()
return [_content_review_suggestion_from_row(row) for row in rows]
def update_suggestion(
self,
*,
article_id: UUID,
review_kind: ContentReviewKind,
suggestion_id: UUID,
status: ReviewSuggestionStatus,
applied_text: str | None,
updated_at: datetime,
) -> ContentReviewSuggestionSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
cursor = connection.execute(
f"""
UPDATE article_review_suggestions
SET
status = {placeholder},
applied_text = {placeholder},
updated_at = {placeholder}
WHERE article_id = {placeholder}
AND review_kind = {placeholder}
AND id = {placeholder}
""",
(
status.value,
applied_text,
_datetime_value(updated_at),
str(article_id),
review_kind.value,
str(suggestion_id),
),
)
if int(getattr(cursor, "rowcount", 0)) <= 0:
raise LookupError(f"Content review suggestion not found: {suggestion_id}")
return self.get_suggestion(
article_id=article_id,
review_kind=review_kind,
suggestion_id=suggestion_id,
)
class AssetsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
@@ -2707,6 +3125,46 @@ def _draft_summary_from_row(row: Any) -> DraftSummary:
)
def _content_review_report_from_row(
row: Any,
*,
issues: list[ContentReviewIssueSummary],
) -> ContentReviewReportSummary:
return ContentReviewReportSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
review_kind=_row_value(row, "review_kind"),
draft_id=_row_value(row, "draft_id"),
score=_row_value(row, "score"),
issues=issues,
recommended_slug=_row_value(row, "recommended_slug"),
recommended_title=_row_value(row, "recommended_title"),
schema_json=_json_from_row(row, "schema_json"),
rules_snapshot=_json_from_row(row, "rules_snapshot"),
created_at=_row_value(row, "created_at"),
)
def _content_review_suggestion_from_row(row: Any) -> ContentReviewSuggestionSummary:
return ContentReviewSuggestionSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
review_kind=_row_value(row, "review_kind"),
report_id=_row_value(row, "report_id"),
suggestion_key=_row_value(row, "suggestion_key"),
severity=_row_value(row, "severity"),
location=_row_value(row, "location"),
message=_row_value(row, "message"),
suggested_fix=_row_value(row, "suggested_fix"),
suggested_rewrite=_row_value(row, "suggested_rewrite"),
patch=_json_from_row(row, "patch"),
status=_row_value(row, "status"),
applied_text=_row_value(row, "applied_text"),
created_at=_row_value(row, "created_at"),
updated_at=_row_value(row, "updated_at"),
)
def _asset_revision_from_row(row: Any) -> AssetRevisionSummary:
return AssetRevisionSummary(
id=_row_value(row, "id"),