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
@@ -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"),