Présence au périmètre : spans de contrat par RSC, fermeture sur le code de sortie
Statut
Accepté — grill /grill-with-docs du 2026-07-01, adossé à un spike sur le C15 réel
(2204 événements, 1372 PDL, 1413 RSC ; repro : 0052-spike-perimetre.py). Premier consommateur :
la commande bot /perimetre pdls → CSV des PDL à déposer sur le portail SGE pour une
demande M023 (collecte des relevés quotidiens → R64).
- Prolonge ADR-0019 (pipeline pur :
la date de référence est un paramètre, l'aujourd'hui est injecté au boundary — comme
affaires_ouvertes(maintenant=…)), ADR-0042 (bornes de période = jour civilDATE, pas des instants). - S'appuie sur les constantes canoniques
ENTREES_C15/SORTIES_C15du registre loader (core/loaders/duckdb/registry.py) et les vuesc15().entrees()/.sorties(). - Respecte ADR-0016 : périmètre = donnée réseau pure, aucune dépendance ERP.
Contexte
Besoin récurrent — et jusqu'ici sans primitive : « quels PDL sont présents dans le périmètre à une date ? ». Le premier appel est la demande M023 (liste des PDL du fournisseur pour activer la collecte quotidienne R64), mais l'objet resservira (rapprochement, reporting, « depuis quand », chevauchements).
Ce qui existe ne répond pas :
abonnementsconstruit des périodes bornées viashift(-1)sur la RSC — ce qui jette la dernière période ouverte ; les contrats vivants ne sont bornés que parce que la spine injecte une grille FACTURATION mensuelle. Inadapté à « qui est là aujourd'hui ».c15().entrees()/.sorties()donnent les événements bruts, pas l'appartenance courante.
Deux signaux candidats pour décider « présent ou sorti » : (1) les codes evenement_declencheur
d'entrée {PMES, MES, CFNE} / de sortie {RES, CFNS} ; (2) le flag etat_contractuel
(EN SERVICE / RESILIE). La décision entre les deux était un inconnu empirique → spike.
Ce que le spike a prouvé (C15 réel)
| Sonde | Résultat |
|---|---|
etat_contractuel |
EN SERVICE 2015 · RESILIE 189 · 0 null |
| Codes de sortie | RES→RESILIE 141 · CFNS→RESILIE 47 (100 % RESILIE) |
| Accord flag ⟺ code au grain RSC | 188 = 188, zéro désaccord |
| Dernier evt / RSC : actif_flag vs actif_code | 0 désaccord |
| Périmètre actif du jour (grain PDL) | flag 1216 = code 1216 |
| Après une sortie, dans la même RSC | 0 événement — jamais rouverte |
| Ré-entrées | toujours une nouvelle RSC (0 entrée post-sortie) |
| Multi-RSC / PDL | 1335×1 · 34×2 · 2×3 · 1×4 |
Nuance isolée : un MDPRM | RESILIE le même jour que la RES (contrat PMES→RES en 3 jours,
emménagement avorté) — le flag peut baver sur une modif coïncidente, donc les codes portent
la date de fin (événement unique), pas le flag.
Décision
- Objet = span de présence, un par RSC :
[debut, fin). Grain RSC (ref_situation_contractuelle) ; la projection en PDL distincts est faite par le consommateur (un PDL multi-RSC — ré-entrée, changement de fournisseur — reste une ligne). debut= jour du 1er événement de la RSC (robuste à une entrée antérieure à notre fenêtre de données).fin= jour du code sortie{RES, CFNS}, ounull(encore présente).- Le flag
etat_contractueln'est pas la source de la date : il sert d'invariant de cohérence (« unRESILIE⟺ une sortie au grain RSC » — vrai à 100 % au spike, à surveiller par un test data si Enedis diverge). ⚠️niveau_ouverture_servicesest autre chose (niveau d'ouverture des services, routageenergie) — jamais l'appartenance au périmètre. - Bornes en jour civil
DATE(Europe/Paris, ADR-0042), intervalle demi-ouvert :actif_à(D)⟺debut ≤ Det (finnulle ouD < fin). Résilié le J ⟹ absent dès J. - Pureté : la date de référence est un paramètre explicite ; l'aujourd'hui (impur) est injecté au boundary (endpoint API).
Implémentation : core/pipelines/perimetre.py — presence_perimetre(c15) -> [rsc, pdl, debut, fin]
et pdls_actifs_a(c15, a_date) -> [pdl].
Conséquences
- Primitive réutilisable : « actif à D » tombe en un
.filter(...).select("pdl").unique(); les spans resservent tels quels ailleurs. - Invariant à surveiller : l'accord flag ⟺ code est un contrat de données (candidat à un test
dbt sur
flux_c15au grain RSC), pas juste une hypothèse. - Premier livrable :
GET /perimetre/pdls.csv?jour=…(défaut : aujourd'hui) → CSV une colonnepdl, exposé au bot par/perimetre pdls.
Alternatives écartées
- « Dernier événement du PDL ∉ {RES, CFNS} » (impl initiale) : grain PDL, sans date, énumère les sorties, perd la notion de span. Correct sur l'instant présent, mais referme la porte.
- Fermer
finsur la bascule du flag : bave possible (MDPRM|RESILIEcoïncident) ; les codes donnent un événement unique et net. Le flag reste, en garde-fou.