Task 011 implement evidence matrix and claim gate

This commit is contained in:
2026-05-21 22:52:41 +03:00
parent 7d4a79acb5
commit 7e1b05067c
23 changed files with 2090 additions and 17 deletions
+4
View File
@@ -61,12 +61,16 @@ def get_article_detail(
boundary_questions = repository.boundary_questions.list_for_article(article_id)
plans = repository.article_plans.list_for_article(article_id)
research_manifests = repository.research_manifests.list_for_article(article_id)
evidence = repository.evidence_items.list_for_article(article_id)
claims = repository.claims.list_for_article(article_id)
return ArticleDetailResponse(
article=article,
target_site=target_site,
workflow_events=workflow_events,
boundary_questions=boundary_questions,
plan=plans[-1] if plans else None,
evidence=evidence,
claims=claims,
research_manifests=research_manifests,
)
+224
View File
@@ -0,0 +1,224 @@
from __future__ import annotations
from datetime import UTC, datetime
from uuid import UUID
from src.domain.contracts import (
AgentJobListResponse,
AgentJobStatus,
AgentJobType,
ArticleCreateResponse,
ArticleWorkflowStatus,
ClaimRiskLevel,
ClaimSupportStatus,
EvidenceMatrixResponse,
EvidenceResponse,
EvidenceCreateRequest,
EvidenceUpdateRequest,
)
def get_evidence_matrix(repository: object, *, article_id: UUID) -> EvidenceMatrixResponse:
article = repository.articles.get(article_id)
manifests = repository.research_manifests.list_for_article(article_id)
if not manifests:
return EvidenceMatrixResponse(article=article)
if not repository.claims.list_for_article(article_id):
_build_matrix(repository, article=article, manifest=manifests[-1])
article = repository.articles.get(article_id)
evidence = repository.evidence_items.list_for_article(article_id)
claims = repository.claims.list_for_article(article_id)
reasons = _insufficient_reasons(claims)
if reasons and article.status != ArticleWorkflowStatus.PLAN_REVISION_REQUIRED:
article = repository.articles.update_status(
article_id=article_id,
status=ArticleWorkflowStatus.PLAN_REVISION_REQUIRED,
updated_at=_now(),
)
repository.articles.create_workflow_event(
article_id=article_id,
event_type="INSUFFICIENT_EVIDENCE_FOUND",
from_status=ArticleWorkflowStatus.RESEARCH_RUNNING,
to_status=ArticleWorkflowStatus.PLAN_REVISION_REQUIRED,
actor_user_id=None,
payload={"reasons": reasons},
created_at=_now(),
)
return EvidenceMatrixResponse(
article=article,
evidence=evidence,
claims=claims,
insufficient_evidence_reasons=reasons,
)
def update_evidence(
repository: object,
*,
article_id: UUID,
evidence_id: UUID,
request: EvidenceUpdateRequest,
) -> EvidenceResponse:
repository.articles.get(article_id)
evidence = repository.evidence_items.update_review_status(
article_id=article_id,
evidence_id=evidence_id,
review_status=request.review_status or "PENDING",
)
return EvidenceResponse(evidence=evidence)
def add_evidence(
repository: object,
*,
article_id: UUID,
request: EvidenceCreateRequest,
) -> EvidenceResponse:
repository.articles.get(article_id)
now = _now()
evidence = repository.evidence_items.create(
article_id=article_id,
source_title=request.source_title,
source_url=request.source_url,
source_type=request.source_type,
source_quality_score=request.source_quality_score,
summary=request.summary,
supports_claims=[],
artifact_manifest_id=_resolve_manifest_id(repository, article_id=article_id),
retrieved_at=now,
review_status="PENDING",
)
if request.claim_text:
repository.claims.create(
article_id=article_id,
section_id=request.section_id,
claim_text=request.claim_text,
support_status=request.support_status,
risk_level=request.risk_level,
evidence_item_ids=[evidence.id],
created_at=now,
)
return EvidenceResponse(evidence=evidence)
def remove_evidence(
repository: object,
*,
article_id: UUID,
evidence_id: UUID,
) -> None:
repository.articles.get(article_id)
evidence = repository.evidence_items.get(evidence_id)
if evidence.article_id != article_id:
raise LookupError(f"Evidence not found: {evidence_id}")
for claim in repository.claims.list_for_article(article_id):
if evidence_id not in claim.evidence_item_ids:
continue
remaining_ids = [item_id for item_id in claim.evidence_item_ids if item_id != evidence_id]
repository.claims.update_evidence_links(
claim_id=claim.id,
evidence_item_ids=remaining_ids,
support_status=(
claim.support_status
if remaining_ids
else ClaimSupportStatus.UNSUPPORTED
),
)
repository.evidence_items.delete(article_id=article_id, evidence_id=evidence_id)
def start_draft(repository: object, *, article_id: UUID) -> AgentJobListResponse:
matrix = get_evidence_matrix(repository, article_id=article_id)
if matrix.insufficient_evidence_reasons:
raise PermissionError("Acceptable evidence is required before draft production")
job = repository.agent_jobs.create(
article_id=article_id,
parent_job_id=None,
attempt=1,
job_type=AgentJobType.DRAFT_ASSEMBLY,
agent_profile="fake-draft-assembly",
status=AgentJobStatus.QUEUED,
input_files=[{"path": "inputs/evidence-matrix.json", "content_hash": None}],
queued_at=_now(),
)
return AgentJobListResponse(jobs=[job])
def approve_final(repository: object, *, article_id: UUID) -> ArticleCreateResponse:
matrix = get_evidence_matrix(repository, article_id=article_id)
high_risk_unsupported = [
claim
for claim in matrix.claims
if claim.support_status == ClaimSupportStatus.UNSUPPORTED
and claim.risk_level == ClaimRiskLevel.HIGH
]
if high_risk_unsupported:
raise PermissionError("High-risk unsupported claims must be resolved")
return ArticleCreateResponse(article=matrix.article)
def _build_matrix(repository: object, *, article: object, manifest: object) -> None:
now = _now()
insufficient = "insufficient" in (
f"{article.brief_description} {article.primary_keyword or ''}".lower()
)
plan = repository.article_plans.list_for_article(article.id)[-1]
first_section_id = plan.sections[0].id if plan.sections else None
if insufficient:
repository.claims.create(
article_id=article.id,
section_id=first_section_id,
claim_text="High-risk factual claim without acceptable source support.",
support_status=ClaimSupportStatus.UNSUPPORTED,
risk_level=ClaimRiskLevel.HIGH,
evidence_item_ids=[],
created_at=now,
)
for artifact in manifest.artifacts:
metadata = artifact.metadata
evidence = repository.evidence_items.create(
article_id=article.id,
source_title=str(metadata.get("source_title", "Research source")),
source_url=artifact.source_url,
source_type=str(metadata.get("source_type", artifact.artifact_type)),
source_quality_score=0.86,
summary=str(metadata.get("summary", "Research source summary")),
supports_claims=[],
artifact_manifest_id=manifest.id,
retrieved_at=now,
)
repository.claims.create(
article_id=article.id,
section_id=first_section_id,
claim_text=str(metadata.get("summary", "Supported factual claim")),
support_status=ClaimSupportStatus.SUPPORTED,
risk_level=ClaimRiskLevel.LOW,
evidence_item_ids=[evidence.id],
created_at=now,
)
def _resolve_manifest_id(repository: object, *, article_id: UUID) -> UUID:
manifests = repository.research_manifests.list_for_article(article_id)
if manifests:
return manifests[-1].id
raise ValueError("Research manifest is required before adding manual evidence")
def _insufficient_reasons(claims: list[object]) -> list[str]:
reasons: list[str] = []
for claim in claims:
if claim.support_status == ClaimSupportStatus.UNSUPPORTED:
reasons.append(f"Unsupported claim: {claim.claim_text}")
return reasons
def _now() -> datetime:
return datetime.now(UTC)
@@ -36,6 +36,10 @@ from .models import (
CurrentUserResponse,
DraftSummary,
EvidenceSummary,
EvidenceCreateRequest,
EvidenceMatrixResponse,
EvidenceResponse,
EvidenceUpdateRequest,
PlanSectionSummary,
PlanSectionEditRequest,
PlanListResponse,
@@ -100,6 +104,10 @@ __all__ = [
"CurrentUserResponse",
"DraftSummary",
"EvidenceSummary",
"EvidenceCreateRequest",
"EvidenceMatrixResponse",
"EvidenceResponse",
"EvidenceUpdateRequest",
"PlanReviewStatus",
"PlanListResponse",
"PlanResponse",
@@ -321,6 +321,27 @@ class EvidenceSummary(ContractModel):
supports_claims: list[UUID] = Field(default_factory=list)
artifact_manifest_id: UUID
retrieved_at: datetime
review_status: str = Field(default="PENDING", min_length=1)
class EvidenceUpdateRequest(ContractModel):
review_status: str | None = Field(default=None, pattern="^(PENDING|APPROVED|REJECTED)$")
class EvidenceCreateRequest(ContractModel):
source_title: str = Field(min_length=1)
source_url: str = Field(min_length=1)
source_type: str = Field(min_length=1)
summary: str = Field(min_length=1)
source_quality_score: float = Field(ge=0, le=1, default=0.7)
section_id: UUID | None = None
claim_text: str | None = None
support_status: ClaimSupportStatus = ClaimSupportStatus.SUPPORTED
risk_level: ClaimRiskLevel = ClaimRiskLevel.LOW
class EvidenceResponse(ContractModel):
evidence: EvidenceSummary
class ClaimSummary(ContractModel):
@@ -333,6 +354,13 @@ class ClaimSummary(ContractModel):
evidence_item_ids: list[UUID] = Field(default_factory=list)
class EvidenceMatrixResponse(ContractModel):
article: ArticleSummary
evidence: list[EvidenceSummary] = Field(default_factory=list)
claims: list[ClaimSummary] = Field(default_factory=list)
insufficient_evidence_reasons: list[str] = Field(default_factory=list)
class DraftSummary(ContractModel):
id: UUID
article_id: UUID
@@ -42,6 +42,10 @@ from .models import (
CurrentUserResponse,
DraftSummary,
EvidenceSummary,
EvidenceCreateRequest,
EvidenceMatrixResponse,
EvidenceResponse,
EvidenceUpdateRequest,
PlanSectionSummary,
PlanSectionEditRequest,
PlanListResponse,
@@ -122,7 +126,11 @@ CONTRACT_SCHEMA_MODELS: tuple[type[BaseModel], ...] = (
ResearchStartResponse,
ResearchListResponse,
EvidenceSummary,
EvidenceCreateRequest,
EvidenceUpdateRequest,
EvidenceResponse,
ClaimSummary,
EvidenceMatrixResponse,
DraftSummary,
AssetSummary,
ReviewSummary,
@@ -15,6 +15,10 @@ from src.domain.contracts import (
AgentJobType,
ArticleSummary,
BoundaryQuestionSummary,
ClaimRiskLevel,
ClaimSupportStatus,
ClaimSummary,
EvidenceSummary,
PlanReviewStatus,
PlanSectionSummary,
PlanSummary,
@@ -49,6 +53,8 @@ class BackendRepository:
self.boundary_questions = BoundaryQuestionsRepository(self)
self.article_plans = ArticlePlansRepository(self)
self.research_manifests = ResearchManifestsRepository(self)
self.evidence_items = EvidenceItemsRepository(self)
self.claims = ClaimsRepository(self)
self.agent_jobs = AgentJobsRepository(self)
self.script_config_versions = ScriptConfigVersionsRepository(self)
self.script_config_version_events = ScriptConfigVersionAuditEventsRepository(self)
@@ -1123,6 +1129,276 @@ class ResearchManifestsRepository:
return _research_manifest_from_row(row)
class EvidenceItemsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
def create(
self,
*,
article_id: UUID,
source_title: str,
source_url: str,
source_type: str,
source_quality_score: float,
summary: str,
supports_claims: list[UUID],
artifact_manifest_id: UUID,
retrieved_at: datetime,
review_status: str = "PENDING",
) -> EvidenceSummary:
evidence_id = uuid4()
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO evidence_items (
id,
article_id,
source_title,
source_url,
source_type,
source_quality_score,
summary,
supports_claims,
artifact_manifest_id,
retrieved_at,
review_status
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder}
)
""",
(
str(evidence_id),
str(article_id),
source_title,
source_url,
source_type,
source_quality_score,
summary,
_json_value([str(claim_id) for claim_id in supports_claims]),
str(artifact_manifest_id),
_datetime_value(retrieved_at),
review_status,
),
)
return self.get(evidence_id)
def list_for_article(self, article_id: UUID) -> list[EvidenceSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT {self._select_columns()}
FROM evidence_items
WHERE article_id = {placeholder}
ORDER BY retrieved_at, id
""",
(str(article_id),),
).fetchall()
return [_evidence_from_row(row) for row in rows]
def get(self, evidence_id: UUID) -> EvidenceSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT {self._select_columns()}
FROM evidence_items
WHERE id = {placeholder}
""",
(str(evidence_id),),
).fetchone()
if row is None:
raise LookupError(f"Evidence not found: {evidence_id}")
return _evidence_from_row(row)
def update_review_status(
self,
*,
article_id: UUID,
evidence_id: UUID,
review_status: str,
) -> EvidenceSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
connection.execute(
f"""
UPDATE evidence_items
SET review_status = {placeholder}
WHERE article_id = {placeholder} AND id = {placeholder}
""",
(review_status, str(article_id), str(evidence_id)),
)
return self.get(evidence_id)
def delete(
self,
*,
article_id: UUID,
evidence_id: UUID,
) -> None:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
connection.execute(
f"""
DELETE FROM evidence_items
WHERE article_id = {placeholder} AND id = {placeholder}
""",
(str(article_id), str(evidence_id)),
)
def _select_columns(self) -> str:
return """
id,
article_id,
source_title,
source_url,
source_type,
source_quality_score,
summary,
supports_claims,
artifact_manifest_id,
retrieved_at,
review_status
"""
class ClaimsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
def create(
self,
*,
article_id: UUID,
section_id: UUID | None,
claim_text: str,
support_status: ClaimSupportStatus,
risk_level: ClaimRiskLevel,
evidence_item_ids: list[UUID],
created_at: datetime,
) -> ClaimSummary:
claim_id = uuid4()
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO claims (
id,
article_id,
section_id,
claim_text,
support_status,
risk_level,
evidence_item_ids,
created_at
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder}
)
""",
(
str(claim_id),
str(article_id),
_uuid_value(section_id),
claim_text,
support_status.value,
risk_level.value,
_json_value([str(evidence_id) for evidence_id in evidence_item_ids]),
_datetime_value(created_at),
),
)
return self.get(claim_id)
def list_for_article(self, article_id: UUID) -> list[ClaimSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT
id,
article_id,
section_id,
claim_text,
support_status,
risk_level,
evidence_item_ids
FROM claims
WHERE article_id = {placeholder}
ORDER BY id
""",
(str(article_id),),
).fetchall()
return [_claim_from_row(row) for row in rows]
def get(self, claim_id: UUID) -> ClaimSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT
id,
article_id,
section_id,
claim_text,
support_status,
risk_level,
evidence_item_ids
FROM claims
WHERE id = {placeholder}
""",
(str(claim_id),),
).fetchone()
if row is None:
raise LookupError(f"Claim not found: {claim_id}")
return _claim_from_row(row)
def update_evidence_links(
self,
*,
claim_id: UUID,
evidence_item_ids: list[UUID],
support_status: ClaimSupportStatus,
) -> ClaimSummary:
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
UPDATE claims
SET evidence_item_ids = {placeholder}{json_cast}, support_status = {placeholder}
WHERE id = {placeholder}
""",
(
_json_value([str(evidence_id) for evidence_id in evidence_item_ids]),
support_status.value,
str(claim_id),
),
)
return self.get(claim_id)
class AgentJobsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
@@ -1847,6 +2123,36 @@ def _research_manifest_from_row(row: Any) -> ResearchArtifactManifestSummary:
)
def _evidence_from_row(row: Any) -> EvidenceSummary:
return EvidenceSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
source_title=_row_value(row, "source_title"),
source_url=_row_value(row, "source_url"),
source_type=_row_value(row, "source_type"),
source_quality_score=float(_row_value(row, "source_quality_score")),
summary=_row_value(row, "summary"),
supports_claims=[UUID(value) for value in _json_from_row(row, "supports_claims")],
artifact_manifest_id=_row_value(row, "artifact_manifest_id"),
retrieved_at=_row_value(row, "retrieved_at"),
review_status=_row_value(row, "review_status"),
)
def _claim_from_row(row: Any) -> ClaimSummary:
return ClaimSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
section_id=_row_value(row, "section_id"),
claim_text=_row_value(row, "claim_text"),
support_status=_row_value(row, "support_status"),
risk_level=_row_value(row, "risk_level"),
evidence_item_ids=[
UUID(value) for value in _json_from_row(row, "evidence_item_ids")
],
)
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
return AgentJobSummary(
id=_row_value(row, "id"),
+5 -2
View File
@@ -186,9 +186,11 @@ POSTGRES_SCHEMA_STATEMENTS: tuple[str, ...] = (
summary TEXT NOT NULL,
supports_claims JSONB NOT NULL DEFAULT '[]'::jsonb,
artifact_manifest_id UUID,
retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now()
retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
review_status TEXT NOT NULL DEFAULT 'PENDING'
)
""",
"ALTER TABLE evidence_items ADD COLUMN IF NOT EXISTS review_status TEXT NOT NULL DEFAULT 'PENDING'",
f"""
CREATE TABLE IF NOT EXISTS claims (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -495,7 +497,8 @@ SQLITE_SCHEMA_STATEMENTS: tuple[str, ...] = (
summary TEXT NOT NULL,
supports_claims TEXT NOT NULL DEFAULT '[]',
artifact_manifest_id TEXT,
retrieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
retrieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
review_status TEXT NOT NULL DEFAULT 'PENDING'
)
""",
"""
+2
View File
@@ -12,6 +12,7 @@ from src.presentation.routes.agent_jobs import router as agent_jobs_router
from src.presentation.routes.articles import router as articles_router
from src.presentation.routes.auth import router as auth_router
from src.presentation.routes.boundary_questions import router as boundary_questions_router
from src.presentation.routes.evidence import router as evidence_router
from src.presentation.routes.plans import router as plans_router
from src.presentation.routes.sites import router as sites_router
@@ -21,6 +22,7 @@ app.include_router(auth_router)
app.include_router(articles_router)
app.include_router(boundary_questions_router)
app.include_router(plans_router)
app.include_router(evidence_router)
app.include_router(agent_jobs_router)
app.include_router(internal_agent_jobs_router)
app.include_router(sites_router)
@@ -0,0 +1,136 @@
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from src.application.evidence import (
add_evidence,
approve_final,
get_evidence_matrix,
remove_evidence,
start_draft,
update_evidence,
)
from src.domain.auth import EDITOR_OR_ADMIN_ROLES
from src.domain.contracts import (
AgentJobListResponse,
ArticleCreateResponse,
CurrentUser,
EvidenceMatrixResponse,
EvidenceResponse,
EvidenceCreateRequest,
EvidenceUpdateRequest,
)
from src.infrastructure.repositories import BackendRepository
from src.presentation.dependencies import get_repository, require_roles
router = APIRouter(prefix="/api", tags=["evidence"])
@router.get(
"/articles/{article_id}/evidence",
response_model=EvidenceMatrixResponse,
)
def get_article_evidence(
article_id: UUID,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> EvidenceMatrixResponse:
try:
return get_evidence_matrix(repository, article_id=article_id)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
@router.patch(
"/articles/{article_id}/evidence/{evidence_id}",
response_model=EvidenceResponse,
)
def patch_evidence(
article_id: UUID,
evidence_id: UUID,
request: EvidenceUpdateRequest,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> EvidenceResponse:
try:
return update_evidence(
repository,
article_id=article_id,
evidence_id=evidence_id,
request=request,
)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
@router.post(
"/articles/{article_id}/evidence",
response_model=EvidenceResponse,
status_code=status.HTTP_201_CREATED,
)
def post_evidence(
article_id: UUID,
request: EvidenceCreateRequest,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> EvidenceResponse:
try:
return add_evidence(repository, article_id=article_id, request=request)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
except ValueError as error:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
@router.delete(
"/articles/{article_id}/evidence/{evidence_id}",
status_code=status.HTTP_200_OK,
)
def delete_evidence(
article_id: UUID,
evidence_id: UUID,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> None:
try:
remove_evidence(repository, article_id=article_id, evidence_id=evidence_id)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
@router.post(
"/articles/{article_id}/draft/start",
response_model=AgentJobListResponse,
status_code=status.HTTP_202_ACCEPTED,
)
def post_start_draft(
article_id: UUID,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> AgentJobListResponse:
try:
return start_draft(repository, article_id=article_id)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
except PermissionError as error:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
@router.post(
"/articles/{article_id}/final/approve",
response_model=ArticleCreateResponse,
)
def post_final_approve(
article_id: UUID,
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
repository: BackendRepository = Depends(get_repository),
) -> ArticleCreateResponse:
try:
return approve_final(repository, article_id=article_id)
except LookupError as error:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
except PermissionError as error:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error