"""Data quality engine — per-entity scores (documented, versioned). Not truth, just transparency: source_count · primary_source_ratio · freshness · field_completeness · agreement (1 − conflicts share) → score 0–100.""" from __future__ import annotations from typing import Any from aiatlas.db import execute, fetch_all, jsonb, transaction QUALITY_VERSION = "1.0" EXPECTED_FIELDS: dict[str, list[str]] = { "model": ["release_date", "openness", "license", "parameter_count", "context_length", "modalities", "architecture", "official_url", "description"], "company": ["country", "founded", "website", "description", "headquarters"], "organization": ["country", "website", "description"], "paper": ["authors", "published_at", "abstract", "arxiv_id", "categories"], "provider": ["website", "description"], "benchmark": ["description", "metric", "category", "website"], "hardware": ["kind", "memory_gb", "memory_bandwidth_gbs", "release_date", "manufacturer"], "framework": ["repository_url", "latest_version", "license", "description", "language"], "dataset": ["license", "modality", "size", "publisher"], "tool": ["website", "category", "description"], "repository": ["repository_url", "license", "description", "language"], } METRIC_DEFINITIONS = [ ("quality_score", "Data quality score", QUALITY_VERSION, "Composite 0–100 transparency score: 0.25·completeness + 0.25·primary_source_ratio + 0.2·freshness + 0.15·agreement + 0.15·source_diversity. " "It measures how well AI Atlas knows an entity, not how good the entity is.", "score = 100·(0.25·completeness + 0.25·primary_ratio + 0.20·freshness + 0.15·agreement + 0.15·min(1, sources/4))"), ("freshness", "Freshness", QUALITY_VERSION, "1 when the entity was confirmed by a source in the last 7 days, decaying linearly to 0 at 180 days.", None), ("completeness", "Field completeness", QUALITY_VERSION, "Share of the expected fields for the entity type that have a current claim.", None), ("primary_source_ratio", "Primary source ratio", QUALITY_VERSION, "Share of current claims backed by a tier-1 (official) source.", None), ] async def recompute(*, entity_ids: list[str] | None = None, limit: int = 20000) -> dict[str, Any]: async with transaction() as conn: for key, label, version, desc, formula in METRIC_DEFINITIONS: await execute(conn, """insert into metric_definitions (key, label, version, description, formula) values (:k, :l, :v, :d, :f) on conflict (key) do update set label = excluded.label, version = excluded.version, description = excluded.description, formula = excluded.formula""", k=key, l=label, v=version, d=desc, f=formula) where = "where e.merged_into is null" + (" and e.id = any(cast(:ids as text[]))" if entity_ids else "") rows = await fetch_all(conn, f""" select e.id, e.entity_type, e.attributes, e.last_seen_at, coalesce((e.quality->>'conflicts')::int, 0) as conflicts, (select count(distinct c.source_id) from claims c where c.entity_id = e.id and c.status = 'current') as source_count, (select count(*) from claims c where c.entity_id = e.id and c.status = 'current') as claim_count, (select count(*) from claims c where c.entity_id = e.id and c.status = 'current' and c.tier = 1) as primary_count, (select count(*) from relations r where (r.subject_id = e.id or r.object_id = e.id) and r.valid_to is null) as relation_count, (select count(*) from change_events ev where ev.entity_id = e.id) as event_count, extract(epoch from now() - e.last_seen_at) / 86400.0 as age_days from entities e {where} order by e.updated_at desc limit :lim""", ids=entity_ids or [], lim=limit) updated = 0 for r in rows: expected = EXPECTED_FIELDS.get(r["entity_type"], ["description"]) attrs = r["attributes"] or {} completeness = sum(1 for f in expected if attrs.get(f) not in (None, "", [], {})) / max(1, len(expected)) primary_ratio = (r["primary_count"] / r["claim_count"]) if r["claim_count"] else 0.0 age = float(r["age_days"] or 0) freshness = 1.0 if age <= 7 else max(0.0, 1 - (age - 7) / 173) agreement = max(0.0, 1 - (r["conflicts"] / max(1, r["claim_count"]))) if r["claim_count"] else 1.0 diversity = min(1.0, (r["source_count"] or 0) / 4) score = round(100 * (0.25 * completeness + 0.25 * primary_ratio + 0.20 * freshness + 0.15 * agreement + 0.15 * diversity)) quality = {"version": QUALITY_VERSION, "score": score, "completeness": round(completeness, 3), "primary_source_ratio": round(primary_ratio, 3), "freshness": round(freshness, 3), "agreement": round(agreement, 3), "source_count": int(r["source_count"] or 0), "claim_count": int(r["claim_count"] or 0), "conflicts": int(r["conflicts"] or 0)} counts = {"relations": int(r["relation_count"] or 0), "events": int(r["event_count"] or 0), "claims": int(r["claim_count"] or 0)} await execute(conn, "update entities set quality = quality || cast(:q as jsonb), counts = cast(:c as jsonb) where id = :id", q=jsonb(quality), c=jsonb(counts), id=r["id"]) updated += 1 return {"updated": updated} __all__ = ["EXPECTED_FIELDS", "QUALITY_VERSION", "recompute"]