# ============================================================================== # Author: Simon-Pierre Boucher # File: creaka/identity.py # Desc: Résolution d'identité — rattacher les bons comptes à la bonne personne # avec un score de confiance par signal (CLAUDE.md §12.1) # ============================================================================== """Résolution d'identité : LE cœur du projet. Chaque rattachement de compte porte un score `confidence` selon le signal qui l'a produit (du plus fort au plus faible, §12.1). Sous `REVIEW_THRESHOLD`, le compte est marqué « à vérifier » et EXCLU de la fiche publique plutôt que rattaché à tort — mieux vaut un lien vérifié de moins qu'un lien erroné. """ from __future__ import annotations from .schema import PlatformAccount # signaux §12.1, du plus fort au plus faible SIGNAL_CONFIDENCE: dict[str, float] = { "profil_source": 0.98, # le compte DONT la fiche est issue (attribution inhérente) "link_in_bio": 0.95, # page Linktree/Beacons du créateur listant le compte "api_officielle": 0.95, # confirmé par l'API officielle de la plateforme "cross_link": 0.90, # un profil vérifié pointe vers l'autre (bio IG → TikTok) "listes_medias": 0.85, # listes/palmarès médias croisés (plusieurs sources concordantes) "base_publique": 0.85, # base de connaissances publique curée (Wikidata/Wikipédia) "handle_match": 0.75, # même handle + même nom d'affichage sur 2 plateformes "mention": 0.60, # mentions réciproques / mêmes liens externes "inconnu": 0.40, } # sous ce seuil : « à vérifier », jamais publié comme rattaché REVIEW_THRESHOLD = 0.50 # à partir de ce seuil : compte considéré fiable pour la déduplication (§12.2) STRONG_THRESHOLD = 0.75 def confidence_for(signal: str) -> float: return SIGNAL_CONFIDENCE.get(signal, SIGNAL_CONFIDENCE["inconnu"]) def account(platform: str, handle: str, signal: str, *, followers: int | None = None, url: str = "", verified: bool | None = None) -> PlatformAccount: """Fabrique un compte avec la confiance du signal fourni (helper connecteurs).""" return PlatformAccount(platform=platform, handle=handle, url=url, followers=followers, verified=verified, confidence=confidence_for(signal), signal=signal) def needs_review(acc: PlatformAccount) -> bool: return (acc.confidence or 0.0) < REVIEW_THRESHOLD def is_strong(acc: PlatformAccount) -> bool: return (acc.confidence or 0.0) >= STRONG_THRESHOLD def merge_accounts(existing: list[PlatformAccount], incoming: list[PlatformAccount]) -> list[PlatformAccount]: """Union de deux listes de comptes — on ne perd AUCUN lien vérifié (§12.2). Même compte (plateforme+handle) : la meilleure confiance gagne ; les champs factuels (abonnés, badge) prennent la valeur la plus fraîche non nulle. """ by_key: dict[str, PlatformAccount] = {a.key: a for a in existing} for acc in incoming: prev = by_key.get(acc.key) if prev is None: by_key[acc.key] = acc continue best = acc if (acc.confidence or 0) >= (prev.confidence or 0) else prev other = prev if best is acc else acc if best.followers is None: best.followers = other.followers if best.verified is None: best.verified = other.verified best.metrics = {**other.metrics, **best.metrics} best.last_checked = max(best.last_checked, other.last_checked) by_key[acc.key] = best return sorted(by_key.values(), key=lambda a: (a.followers is None, -(a.followers or 0)))