Initial import of Politrack, a self-hosted service that tracks Austrian politicians from federal to municipal level - roles, votes, money flows, statements, meetings, social media and legal proceedings - with every claim linked to its sources. - backend: FastAPI + Postgres/pgvector; adapter framework (fetch -> parse -> load -> enrich) with robots.txt/TDM-aware fetching, a raw-response archive for reparsing, and a claim/evidence model with corroboration - sources: Parliament, Wikidata, EU Parliament/Commission, Council of Ministers, Rechnungshof party reports and donations, procurement, RTR media transparency, political ad libraries, WKStA, news feeds, Wikipedia edits, Bluesky, Mastodon, public Telegram channels, YouTube and X (official API) - web: Next.js frontend in German and English with typed API client - mcp: MCP server for AI agents and RAG - docs: architecture, data model, sources, legal notes, Unraid deployment and contributor guides; Docker Compose setup for Unraid Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
1.8 KiB
Python
42 lines
1.8 KiB
Python
"""Rules shared by all read queries.
|
|
|
|
* Source filtering happens at the evidence level: a claim is returned only if at least one document comes
|
|
from an allowed source, and only allowed evidence is shown.
|
|
* Claims merged into another claim (corroboration) are hidden; the survivor carries all evidence.
|
|
* Claims pending human review are hidden unless include_pending is set (moderation).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
VISIBLE = ("c.merged_into IS NULL AND (c.review_status = 'published' OR "
|
|
"(%(include_pending)s AND c.review_status = 'pending'))")
|
|
|
|
|
|
@dataclass
|
|
class SourceFilter:
|
|
exclude_sources: list[str] = field(default_factory=list)
|
|
min_trust: int = 0
|
|
|
|
def sql(self, alias: str = "s") -> tuple[str, dict[str, Any]]:
|
|
return (
|
|
f"(NOT ({alias}.slug = ANY(%(exclude_sources)s)) AND {alias}.default_trust >= %(min_trust)s)",
|
|
{"exclude_sources": self.exclude_sources, "min_trust": self.min_trust},
|
|
)
|
|
|
|
|
|
def evidence_json(source_sql: str) -> str:
|
|
"""Lateral subquery aggregating the allowed evidence of claim `c`."""
|
|
return f"""
|
|
CROSS JOIN LATERAL (
|
|
SELECT jsonb_agg(jsonb_build_object(
|
|
'url', d.url, 'title', d.title, 'source', s.slug, 'source_name', s.name, 'source_kind', s.kind,
|
|
'published_at', d.published_at, 'quote', e.quote, 'stance', e.stance, 'archive_url', d.archive_url)
|
|
ORDER BY s.default_trust DESC, d.published_at) AS evidence,
|
|
count(DISTINCT s.id) AS source_count
|
|
FROM claim_evidence e JOIN document d ON d.id = e.document_id JOIN source s ON s.id = d.source_id
|
|
WHERE e.claim_id = c.id AND {source_sql}
|
|
) ev"""
|