Picarones / picarones /core /narrative /registry.py
Claude
sprint29: registre déclaratif des détecteurs narratifs (decorator-based)
92de89a unverified
Raw
History Blame
7.72 kB
"""Registre déclaratif des détecteurs narratifs (Sprint 29).
Avant le Sprint 29, ajouter un nouveau type de fait imposait de toucher
**quatre** fichiers :
1. ``facts.py`` — ajouter une valeur à ``FactType`` ;
2. ``detectors.py`` — écrire ``def detect_xxx(data) -> list[Fact]`` ;
3. ``detectors.py`` — l'inscrire dans le dict ``DETECTORS_BY_TYPE`` ;
4. ``arbiter.py`` — ajouter le type à la séquence ``DEFAULT_TYPE_ORDER``
au bon endroit pour la priorité éditoriale.
Sprint 29 ramène le nombre de modifications à **deux** :
1. ``facts.py`` — toujours nécessaire pour le type énuméré ;
2. ``detectors.py`` — décorer la fonction avec ``@register_detector(...)``.
Le décorateur :
- enregistre la fonction dans un registre global trié par ``priority`` ;
- vérifie qu'aucun détecteur ne se réenregistre sur le même ``FactType`` ;
- laisse la fonction utilisable telle quelle (rétrocompatibilité) ;
- alimente automatiquement ``arbiter.DEFAULT_TYPE_ORDER``.
Conventions de priorité (« politique éditoriale » du rapport)
-------------------------------------------------------------
Plus la valeur est petite, plus le fait remonte tôt en synthèse à
importance égale. Pour conserver l'ordre historique du Sprint 23, on
utilise un pas de 10 pour laisser de la place à des insertions futures :
10 GLOBAL_LEADER_CER qui gagne globalement
20 STATISTICAL_TIE y a-t-il un ex-aequo
30 SIGNIFICANT_GAP à quel point l'écart est solide
40 STRATUM_WINNER qui domine sur quel sous-corpus
50 STRATUM_COLLAPSE qui s'effondre sur quoi
60 ERROR_PROFILE_OUTLIER qui se trompe différemment
70 LLM_HALLUCINATION_FLAG hallucinations VLM
80 ROBUSTNESS_FRAGILE sensibilité aux dégradations
90 PARETO_ALTERNATIVE compromis coût/qualité
100 SPEED_WINNER vitesse
110 COST_OUTLIER coût aberrant
120 CONFIDENCE_WARNING mise en garde sur la fiabilité
Le décorateur n'impose **pas** de pas — un détecteur tiers peut très
bien utiliser ``priority=42`` pour s'insérer entre STRATUM_WINNER et
STRATUM_COLLAPSE par exemple.
"""
from __future__ import annotations
import logging
import threading
from dataclasses import dataclass
from typing import Callable, Optional
from picarones.core.narrative.facts import (
DetectorFn,
DetectorRegistry,
FactImportance,
FactType,
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Métadonnées d'un détecteur
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class DetectorEntry:
"""Métadonnées d'un détecteur enregistré."""
fact_type: FactType
fn: DetectorFn
priority: int
importance: FactImportance
# ---------------------------------------------------------------------------
# Registre global
# ---------------------------------------------------------------------------
_REGISTRY: dict[FactType, DetectorEntry] = {}
_REGISTRY_LOCK = threading.Lock()
def register_detector(
fact_type: FactType,
*,
priority: int,
importance: FactImportance = FactImportance.MEDIUM,
) -> Callable[[DetectorFn], DetectorFn]:
"""Décorateur d'enregistrement.
Usage::
@register_detector(FactType.GLOBAL_LEADER_CER, priority=10,
importance=FactImportance.CRITICAL)
def detect_global_leader_cer(data: dict) -> list[Fact]:
...
Le décorateur :
- vérifie qu'aucun autre détecteur n'est déjà enregistré sur
``fact_type`` (sinon ``ValueError``) ;
- vérifie que ``priority`` est un entier ;
- retourne la fonction inchangée pour ne pas casser les imports
existants.
L'``importance`` mémorisée ici sert de **métadonnée** au registre :
chaque détecteur reste libre d'émettre des ``Fact`` avec une
importance différente selon le contexte (ex. CRITICAL si l'écart
est gigantesque, HIGH sinon).
"""
def _decorator(fn: DetectorFn) -> DetectorFn:
with _REGISTRY_LOCK:
if fact_type in _REGISTRY:
raise ValueError(
f"Détecteur déjà enregistré pour {fact_type.value!r} : "
f"{_REGISTRY[fact_type].fn.__name__}. Désenregistrer "
"explicitement avant de réassigner."
)
entry = DetectorEntry(
fact_type=fact_type,
fn=fn,
priority=int(priority),
importance=importance,
)
_REGISTRY[fact_type] = entry
logger.debug(
"[narrative.registry] enregistré %s priority=%s importance=%s",
fact_type.value, priority, importance.name,
)
return fn
return _decorator
def unregister(fact_type: FactType) -> None:
"""Retire un détecteur du registre — utilisé par les tests."""
with _REGISTRY_LOCK:
_REGISTRY.pop(fact_type, None)
def iter_detectors() -> list[DetectorEntry]:
"""Retourne tous les détecteurs enregistrés, triés par ``priority``.
Le tri est stable : à ``priority`` égale, l'ordre d'enregistrement
est préservé (utile en présence d'extensions tierces).
"""
with _REGISTRY_LOCK:
entries = list(_REGISTRY.values())
entries.sort(key=lambda e: e.priority)
return entries
def detector_for(fact_type: FactType) -> Optional[DetectorEntry]:
with _REGISTRY_LOCK:
return _REGISTRY.get(fact_type)
def clear_registry() -> None:
"""Vide le registre — réservé aux tests d'isolation."""
with _REGISTRY_LOCK:
_REGISTRY.clear()
def default_type_order() -> tuple[FactType, ...]:
"""Calcule l'ordre canonique des types depuis le registre courant.
Source de vérité de ``arbiter.DEFAULT_TYPE_ORDER`` depuis le Sprint 29.
"""
return tuple(e.fact_type for e in iter_detectors())
# ---------------------------------------------------------------------------
# Pont avec ``DetectorRegistry`` historique
# ---------------------------------------------------------------------------
def populate_legacy_registry(registry: DetectorRegistry) -> None:
"""Synchronise le ``DetectorRegistry`` historique depuis le décorateur.
L'objet ``DetectorRegistry`` reste l'API publique pour les
consommateurs externes (cf. ``DetectorRegistry.run``) ; cette
fonction l'alimente depuis le registre déclaratif courant.
"""
for entry in iter_detectors():
registry.register(entry.fact_type, entry.fn)
__all__ = [
"DetectorEntry",
"register_detector",
"unregister",
"iter_detectors",
"detector_for",
"clear_registry",
"default_type_order",
"populate_legacy_registry",
]
# ---------------------------------------------------------------------------
# Sentinel — sans usage direct ; vérifie au build qu'on n'introduit pas
# de valeur ``priority`` dupliquée par accident parmi les builtins.
# ---------------------------------------------------------------------------
def _verify_unique_priorities() -> None:
seen: dict[int, FactType] = {}
for entry in iter_detectors():
if entry.priority in seen:
logger.warning(
"[narrative.registry] priority %s dupliquée : "
"%s et %s — ordre indéterministe à priorité égale.",
entry.priority,
seen[entry.priority].value,
entry.fact_type.value,
)
else:
seen[entry.priority] = entry.fact_type