File size: 7,718 Bytes
92de89a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
"""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