# Qualité des Données & Contrôles d'Intégrité

Trois garde-fous vérifient la donnée. Ils sont à lancer après toute modification du code d'ingestion, de rattachement ou d'enrichissement, avant de fusionner :

```bash
pnpm --filter @clair/ingestion test                                    # tests unitaires (Vitest)
pnpm --filter @clair/ingestion check-quality                           # contrôles de la base
pnpm --filter @clair/ingestion exec tsx src/cli.ts check-ia-quality    # contrôles des résumés IA
```

Les deux `check-*` interrogent la base pointée par `DATABASE_URL` et sortent avec le code **1** en cas d'échec. Ne pas les faire passer dans un pipe (`| tail`, `| grep`) : le code de sortie serait celui de la dernière commande.

---

## 🛡️ `check-quality`

Code : `services/ingestion/src/checks/data-quality.ts`. Le contrôle réussit si **tous** les invariants et tous les seuils passent.

### Invariants (valeur attendue : 0)

| Contrôle |
| :--- |
| Votes sans scrutin valide |
| Scrutins sans aucun vote (hors Congrès, voir plus bas) |
| Doublons de scrutins (numéro + chambre + session) |
| Doublons d'amendements (uid) |
| Amendements en double sous une même clé canonique |
| Prises de parole du Sénat empilées sur un même rang de séance |
| Tables sans `updated_at` tenu par la base (colonne ou trigger manquant, voir [Suivi des modifications](/donnees/suivi-des-modifications)) |
| Liaisons qui ne datent pas leur parent (cosignataires, amendement ↔ scrutin, secteurs, participants) |
| Parlementaires actifs sans groupe |
| Personnes à la fois député et sénateur en cours de mandat |
| Fiches portant des mandats de l'autre chambre sans son identifiant (homonymes fusionnés : `separer-homonyme` ; vrai changement de chambre : `lier-identifiant`) |
| Mandats d'organe AN ouverts sans mandat de député ouvert |
| Liens scrutin → dossier entre chambres différentes |
| Liens scrutin → dossier entre législatures différentes (AN) |
| Scrutins rattachés à une procédure sans vote : rapport ou mission d'information, ou 49.3 hors motion de censure |
| Scrutins AN rattachés à un autre dossier que celui qui les référence (`voteRefs`) |
| Motions de censure rattachées à un dossier qui ne les référence pas |
| Liens scrutin ↔ amendement entre législatures différentes (AN) |
| Scrutins AN liés à un amendement déposé après le vote |
| Prises de parole rattachées au dossier d'une autre législature (AN) |
| Amendements rattachés au dossier d'une autre législature (AN) |
| Amendements AN rattachés à un autre dossier que celui de leur texte |
| Sous-amendements AN rattachés à un amendement d'un autre texte |
| Amendements placés dans une discussion sans identifiant de discussion |
| Interventions dont la date contredit celle de leur séance |
| Scrutins du Sénat dont la date porte un décalage de fuseau |
| Résumés IA contredisant le statut du sujet |
| Événements à date imprécise dont le mois est passé |
| Candidatures sans nom |
| Circonscriptions aux listes de tailles inégales (art. L. 300 du code électoral) |
| Ruptures d'alternance femme-homme sur les listes (art. L. 300) |
| Binômes titulaire-suppléant de même sexe (art. L. 299) |
| Sortants déclarés par le fichier du ministère mais non rattachés à une personne |
| Index LEGI jamais chargé, ou pas vérifié contre les archives de la DILA depuis plus de 2 jours |
| Amendements avec dispositif, en base depuis plus de 2 jours, dont les renvois au droit en vigueur n'ont pas été calculés par la version courante de la détection |
| Gouvernements qui se chevauchent au-delà du jour de passation |
| Écart à un seul gouvernement en cours |
| Postes ministériels hors des dates de leur gouvernement (un sortant peut finir au plus tard le jour où commence le gouvernement suivant : il expédie les affaires courantes) |
| Postes ministériels qui finissent avant de commencer |
| Gouvernements sans exactement un Premier ministre (poste de rang protocolaire 1) à leurs dates |
| Membres du gouvernement sans poste |
| Membres du gouvernement rattachés à une fiche qui ne leur correspond plus (identifiant AN ou état civil) |
| Fonctions gouvernementales encore dans `mandats` (elles viennent de `postes_ministeriels` et `presidences`) |
| Réponses de membres du Gouvernement typées « question » (elles entreraient dans le compteur de questions posées de leur fiche) |
| Votes exprimés pendant un poste ministériel (un membre du Gouvernement ne vote pas : un tel vote trahit une fiche mal liée ou des dates de poste fausses) |
| Présidences qui se chevauchent au-delà du jour de passation |
| Écart à une seule présidence en cours |
| Gouvernements qui ne commencent pendant aucune présidence |
| Postes ministériels sans domaine : un intitulé nouveau, à ajouter à `domaines-ministeriels.ts` |
| Présidences de l'Assemblée qui se chevauchent au-delà du jour de passation |
| Écart à une seule présidence de l'Assemblée en cours |

**Le Congrès n'a pas de votes nominatifs, c'est normal.** Les scrutins du Congrès (députés et sénateurs réunis à Versailles) sont publiés par l'AN sous un uid `VTCG…` (CG pour Congrès, AN pour Assemblée) avec leurs totaux, mais sans aucune liste de votants. Ce n'est ni une perte à l'ingestion ni un scrutin cassé : il n'y a rien à réparer. Ces scrutins sont donc exclus de l'invariant « scrutins sans aucun vote » (filtre sur `source_url` contenant `/scrutins/VTCG`), ce qui ne peut masquer aucun scrutin de l'Assemblée ni du Sénat. Au 7 octobre 2026, la base en compte un seul : `VTCGR5L16V1`, la révision constitutionnelle sur l'IVG du 4 mars 2024 (780 pour, 72 contre).

### Seuils (valeur minimale)

| Contrôle | Minimum |
| :--- | ---: |
| Parlementaires (toutes les fiches, en exercice ou non) | 900 |
| Groupes politiques | 15 |
| Scrutins | 5 000 |
| Votes | 1 000 000 |
| Amendements | 150 000 |
| Interventions | 500 000 |
| Liens débat → scrutin (Assemblée) | 130 000 |
| Scrutins de l'Assemblée dont on montre le débat | 11 500 |
| Scrutins du Sénat dont on montre le débat | 700 |
| Explications de vote de l'Assemblée | 1 800 |
| Interventions rattachées à un amendement (Assemblée) | 40 000 |
| Lobbyistes | 3 500 |
| Actions de lobbying | 400 |
| Dossiers législatifs | 6 000 |
| Liens amendement ↔ scrutin | 4 000 |
| Taux de liaison scrutins → amendements, AN (scrutins de la législature courante dont le titre mentionne un amendement) | 80 % |
| Taux de liaison scrutins → amendements, Sénat (mêmes scrutins, sessions 2024 et suivantes) | 45 % |
| Taux de liaison scrutins → dossiers (Sénat, et AN de la législature courante) | 90 % |
| Taux de liaison amendements → dossiers | 40 % |
| Amendements du Sénat avec article visé | 90 % |
| Amendements de l'AN datés par la source (`chronotag`) | 99 % |
| Sous-amendements de l'AN dont le parent est retrouvé | 99 % |
| Commissions et organes | 2 900 |
| Commissions et organes de l'Assemblée | 2 800 |
| Commissions et organes du Sénat | 35 |
| Président du Sénat en fonction (exactement 1) | 1 |
| Scrutins solennels du Sénat (votes personnels déduits des délégations) | 30 |
| Concordance des commissions du Sénat en cours avec l'annuaire du jour | 97 % |
| Réunions | 6 000 |
| Taux de liaison réunions → commissions (AN) | 85 % |
| Index LEGI : versions de textes | 140 000 |
| Index LEGI : versions d'articles | 680 000 |
| Amendements calculés dont le dispositif cite au moins un texte ou article lié | 25 % |
| Gouvernements (depuis 2007) | 17 |
| Postes ministériels | 766 |
| Membres du gouvernement | 311 |
| Membres du gouvernement rattachés à leur fiche | 145 |
| Présidences de la République (depuis 2007) | 3 |
| Présidences de l'Assemblée (depuis 2007) | 7 |

Le périmètre des taux de liaison au Sénat commence à la session 2024, parce que les amendements des sessions antérieures ne sont pas ingérés : les compter mesurerait le périmètre de l'ingestion, pas la qualité du rattachement.

### Rapports informatifs (sans effet sur le code de sortie)

- **Intégrité des scrutins multi-amendements** : pour chaque scrutin portant sur des « amendements identiques », les numéros extraits du titre sont comparés aux amendements rattachés (manquants, en trop).
- **Liens des sujets** : couverture des liens « construction » et « contexte », et vérification HTTP d'un échantillon de 10 liens tirés au hasard (délai de 5 s ; une erreur réseau compte comme « non vérifié », pas comme un lien mort).

---

## 🤖 `check-ia-quality`

Code : `services/ingestion/src/checks/ia-quality.ts`. Il relit le champ `enjeux` des **sujets** et le résumé `resume_ia` des **dossiers**, et les confronte aux votes réels des groupes cités.

| Défaut détecté | Seuil maximal |
| :--- | ---: |
| Inversion pour/contre : un groupe décrit comme favorable alors qu'il a voté contre, ou l'inverse | 35 |
| Abstention décrite comme un vote pour ou contre | 45 |
| Position prêtée à un groupe qui n'a aucun vote sur l'entité | 20 |

Le signal « groupe pesant dans les votes mais jamais mentionné » est calculé et affiché, sans effet sur le résultat.

Les votes sont comptés sous le groupe du **mandat** à la date du scrutin, comme dans les prompts : à l'Assemblée, le mandat de la législature du scrutin ; au Sénat, le mandat en cours à sa date. Le générateur et le contrôle partagent le même fragment SQL (`services/ingestion/src/utils/groupe-epoque.ts`) : un résumé de 2018 qui cite LAREM ou LR est jugé sur les groupes de 2018, pas sur ceux d'aujourd'hui.

Les sigles successifs d'une même famille politique (FI, LFI-NUPES et LFI-NFP ; LR, UMP et DR…) sont rapprochés par leurs alias communs. LREM, le groupe du Sénat devenu RDPI, est rattaché à LAREM, car le modèle écrit « LREM » pour le groupe de l'Assemblée.

Les votes sont aussi comptés **chambre par chambre**, car un même sigle ou un même nom désigne deux groupes distincts : « SOC » est le groupe socialiste de l'Assemblée et celui du Sénat, « Les Républicains » le groupe LR de l'Assemblée et le groupe UMP du Sénat. Une phrase qui nomme une chambre est comparée aux votes de cette chambre, et une phrase qui nomme les deux est contrôlée dans les deux. Une phrase qui n'en nomme aucune, alors que le groupe désigné a voté dans le sens décrit dans l'une des deux, est ambiguë mais pas fausse : elle n'est pas comptée. L'intitulé d'un seul des deux groupes (« Socialistes et apparentés ») ou un sigle cité tel quel (« SOC-A ») n'est pas ambigu.

Les règles du contrôle suivent celles du prompt :
- un groupe ne s'abstient majoritairement que si ses abstentions **dépassent** ses voix exprimées. À égalité, le prompt l'annonce favorable ou opposé, et le contrôle ne reproche pas au modèle de l'avoir écrit ;
- un groupe cité comme l'exception d'une phrase (« tous les groupes, à l'exception du CRC, ont voté pour ») ne se voit pas prêter la position des autres ;
- le passif décrit le sort d'un objet, pas le vote d'un groupe : « un amendement du groupe SOC a été rejeté » n'est pas une opposition du groupe.

La détection des inversions repose sur des expressions régulières appliquées au voisinage de chaque nom de groupe. Elle produit des faux positifs, et ses seuils sont documentés dans le code comme des dettes à ramener vers zéro plutôt que comme des tolérances.

---

## 🧪 Tests unitaires

Les tests de l'ingestion (`*.test.ts` à côté des fichiers testés) couvrent notamment les parsers de sources (syceron, comptes rendus de commission, avis de commission, agenda et vidéos du Sénat, candidatures), les rattachements (débats ↔ scrutins, amendement ↔ scrutin, article ↔ scrutin), la nature des scrutins, les prompts IA et la configuration des contrôles de qualité. L'API a ses propres tests Vitest dans `apps/api/src`. Le frontend n'en a qu'un, celui du proxy `/api/v1` (`app/api/v1/[...path]/route.test.ts`) : chemins relayés vers l'API, chemins refusés.

La CI (GitHub Actions) lance le lint, les tests et le build à chaque push et pull request sur `master`. Elle ne lance ni `check-quality` ni `check-ia-quality`, qui ont besoin d'une base remplie.
