Phase 5 : collecteurs officiels et run de veille — dry-run contre sources réelles

Collecteurs, chacun isolé et tolérant à l'échec :
- legifrance : OAuth PISTE validé en production. Deux pièges vérifiés sur le
  compte réel — seul le couple « Client ID / Client secret » ouvre un jeton
  (la clé d'API donne invalid_client), et les identifiants de production ne
  passent pas en bac à sable.
- senat : liste chronologique des lois promulguées. L'URL du §5.1
  (senat.fr/lois/index.html) est morte ; l'équivalent fonctionnel est
  /dossiers-legislatifs/lois-promulguees.html — 31 lois 2026 relevées.
- conseil_constitutionnel : sans le paramètre ?id=32246 la page des affaires en
  instance ne rend que des QPC et aucune affaire DC. Les colonnes DC et QPC
  diffèrent : lecture par en-tête, jamais par position.

Rapprochement à trois issues — numéro officiel, similarité de titre, et une
zone de signalement où le pipeline s'abstient plutôt que de fusionner.

make update-dry contre les sources réelles : aucun faux positif sur les 52
textes du corpus, et trois apports que le rapport n'avait pas — le numéro
d'affaire 2026-907 DC de la programmation militaire, la date de saisine du
16 juillet pour l'aide à mourir, les décisions du 23 juillet sur 908 et 909.

Une asymétrie de sources est documentée par un test plutôt que corrigée en
douce : le Sénat ne qualifie pas la loi 2026-650 d'organique, Légifrance si.
Le collecteur rapporte ce que sa source dit ; le champ  n'est pas comparé.

Fixtures HTML figées : aucun test ne touche au réseau.
156 tests, 91 % de couverture (chaque module au-dessus du seuil de 70 %).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cyber Mawonaj
2026-07-25 21:48:56 -04:00
co-authored by Claude Opus 5
parent c9fc8f3f67
commit feb5544599
15 changed files with 2743 additions and 1 deletions
+163
View File
@@ -0,0 +1,163 @@
<html><body><table class="saisine-list sticky-header responsive-enabled" data-striping="1">
<caption class="sr-only">Tableau des affaires en instance</caption>
<thead>
<tr>
<th scope="col" specifier="field_numero_affaire" aria-sort="ascending" class="is-active"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=desc&amp;order=Affaire%20n%C2%B0" title="trier par Affaire n°" rel="nofollow">Affaire n°<span class="tablesort tablesort--desc">
<span class="visually-hidden">
Trier par ordre décroissant
</span>
</span>
</a></th>
<th scope="col" specifier="field_disposition_loi"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=asc&amp;order=Disposition" title="trier par Disposition" rel="nofollow">Disposition</a></th>
<th scope="col" specifier="field_auteur_saisine"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=asc&amp;order=Auteur%20de%20la%20saisine" title="trier par Auteur de la saisine" rel="nofollow">Auteur de la saisine</a></th>
<th scope="col" specifier="field_dates_saisine"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=asc&amp;order=Date%20de%20la%20saisine" title="trier par Date de la saisine" rel="nofollow">Date de la saisine</a></th>
<th scope="col" specifier="field_date_limite_reception"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=asc&amp;order=Date%20de%20cl%C3%B4ture%20de%20l%27instruction" title="trier par Date de clôture de l'instruction" rel="nofollow">Date de clôture de l'instruction</a></th>
<th scope="col" specifier="field_decision_rendue"><a href="/decisions/affaires-en-instances?id=32246&amp;sort=asc&amp;order=Date%20de%20lecture%20de%20la%20d%C3%A9cision" title="trier par Date de lecture de la décision" rel="nofollow">Date de lecture de la décision</a></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><div class="mobile-hide" data-column="Affaire n°">2026-907 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi actualisant la programmation militaire pour les années 2024 à 2030 et portant diverses dispositions intéressant la défense</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026907dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>06.07.2026</p>
<p class="cell-value hidden">2026-07-06T00:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="even">
<td><div class="mobile-hide" data-column="Affaire n°">2026-910 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi relative au droit à l'aide à mourir</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026910dc_saisinepdtsen.pdf" target="_blank" class="auteur"><p>Président du Sénat</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026910dc_saisinesen_0.pdf" target="_blank" class="auteur"><p>Plus de soixante sénateurs</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026910dc_saisinespm.pdf" target="_blank" class="auteur"><p>Premier ministre</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026910dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026910dc_saisinedep2.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>16.07.2026</p>
<p class="cell-value hidden">2026-07-16T09:00:00+0200</p>
<p>17.07.2026</p>
<p class="cell-value hidden">2026-07-17T11:00:00+0200</p>
<p>17.07.2026</p>
<p class="cell-value hidden">2026-07-17T12:00:00+0200</p>
<p>20.07.2026</p>
<p class="cell-value hidden">2026-07-20T07:00:00+0200</p>
<p>20.07.2026</p>
<p class="cell-value hidden">2026-07-20T08:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="odd">
<td><div class="mobile-hide" data-column="Affaire n°">2026-911 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi visant à protéger les mineurs des risques auxquels les expose lutilisation des réseaux sociaux</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026911dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026911dc_saisinedep2.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>23.07.2026</p>
<p class="cell-value hidden">2026-07-23T00:00:00+0200</p>
<p>24.07.2026</p>
<p class="cell-value hidden">2026-07-24T18:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="even">
<td><div class="mobile-hide" data-column="Affaire n°">2026-912 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi pour une montagne vivante et souveraine</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026912dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>23.07.2026</p>
<p class="cell-value hidden">2026-07-23T00:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="odd">
<td><div class="mobile-hide" data-column="Affaire n°">2026-913 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi visant à moderniser la gestion du patrimoine immobilier de l’État</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026913dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>23.07.2026</p>
<p class="cell-value hidden">2026-07-23T00:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="even">
<td><div class="mobile-hide" data-column="Affaire n°">2026-914 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi d'urgence pour la protection et la souveraineté agricoles</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026914dc_saisinedep.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026914dc_saisinedep_2.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>24.07.2026</p>
<p class="cell-value hidden">2026-07-24T14:00:00+0200</p>
<p>24.07.2026</p>
<p class="cell-value hidden">2026-07-24T15:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
<tr class="odd">
<td><div class="mobile-hide" data-column="Affaire n°">2026-915 DC<span class="arrow"></span>
</div>
</td>
<td><div class="mobile-hide" data-column="Disposition">Loi visant à offrir des réponses immédiates aux phénomènes troublant lordre public, la sécurité et la tranquillité de nos concitoyens</div>
</td>
<td><div class="auteurs" data-column="Auteur de la saisine"><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026915dc_saisinedep1.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a><a href="https://www.conseil-constitutionnel.fr/sites/default/files/2026-07/2026915dc_saisinedep2.pdf" target="_blank" class="auteur"><p>Plus de soixante députés</p>
</a></div>
</td>
<td><div class="date" data-column="Date de la saisine"><p>24.07.2026</p>
<p class="cell-value hidden">2026-07-24T08:00:00+0200</p>
<p>24.07.2026</p>
<p class="cell-value hidden">2026-07-24T09:00:00+0200</p>
</div>
</td>
<td></td>
<td><div class="no-date" data-column="Date de lecture de la décision"></div>
</td>
</tr>
</tbody>
</table></body></html>
+345
View File
@@ -0,0 +1,345 @@
<html><body><article class="landing landing--type-decisions landing--view-mode-full">
<header>
<div class="field field--name-field-image-bandeau field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-bandeau-landing">
<div class="field field--name-field-media-image field--type-image field--label-hidden field__item"> <picture>
<source srcset="/sites/default/files/styles/bandeau_landing_wide/public/2020-07/bg-landing-decisions.jpg.webp?h=06f6671c&amp;itok=Wyn5lULp 1x" media="all and (min-width: 1024px)" type="image/webp" width="1350" height="550">
<source srcset="/sites/default/files/styles/bandeau_landing_narrow/public/2020-07/bg-landing-decisions.jpg.webp?h=06f6671c&amp;itok=yKNGqhX1 1x" media="all and (min-width: 481px) and (max-width: 1023px)" type="image/webp" width="1024" height="417">
<source srcset="/sites/default/files/styles/bandeau_landing_mobile/public/2020-07/bg-landing-decisions.jpg.webp?h=06f6671c&amp;itok=qFsmUyCr 1x" media="all and (max-width: 480px)" type="image/webp" width="480" height="196">
<img loading="lazy" aria-hidden="true" width="480" height="196" src="/sites/default/files/styles/bandeau_landing_mobile/public/2020-07/bg-landing-decisions.jpg.webp?h=06f6671c&amp;itok=qFsmUyCr" alt="Bandeau de la landing Toutes les décisions">
</picture>
</div>
</article>
</div>
<h1 class="title">Les décisions du Conseil Constitutionnel</h1>
<div class="clearfix text-formatted field field--name-field-texte field--type-text-long field--label-hidden field__item"><p>Toutes les décisions, ainsi que des outils statistiques et explicatifs pour mieux connaître lactivité du Conseil constitutionnel depuis sa création</p>
</div>
<div class="field field--name-field-lien-decision field--type-link field--label-hidden field__item"><a href="/les-decisions/decisions-par-type">Comprendre les types de décisions</a></div>
<div class="field field--name-field-lien-affaires-instance field--type-link field--label-hidden field__item"><a href="/decisions/affaires-instances">Les affaires en instance</a></div>
<div class="formulaire-recherche-decisions"><form data-drupal-selector="cc-landing-recherche-decisions" action="/decisions" method="post" id="cc-landing-recherche-decisions" accept-charset="UTF-8">
<h3 class="titre-formulaire" data-drupal-selector="edit-titre">Rechercher une décision</h3>
<div class="js-form-item form-item js-form-type-search form-type-search js-form-item-textfield form-item-textfield">
<label for="edit-textfield">Rechercher un terme ou un numéro de décision</label>
<input list="autocomplete-list" data-drupal-selector="edit-textfield" type="search" id="edit-textfield" name="textfield" value="" size="60" maxlength="128" placeholder="Ex: égalité devant la loi" class="form-search">
</div>
<datalist id="autocomplete-list" data-drupal-selector="edit-autocomplete-list"></datalist>
<div class="js-form-item form-item js-form-type-select form-type-select js-form-item-types form-item-types form-no-label">
<select data-drupal-selector="edit-types" id="edit-types" name="types" class="form-select"><option value="all">Tous les types</option><option value="32205">Question prioritaire de constitutionnalité</option><option value="32246">Constitutionnalité des lois, traités, des règlements des Assemblées</option><option value="32797">Loi ordinaire</option><option value="32800">Loi organique</option><option value="32803">Traité</option><option value="32806">Règlement des assemblées</option><option value="32211">Élections à l'Assemblée nationale</option><option value="32220">Élections au Sénat</option><option value="32224">Loi du pays de Nouvelle-Calédonie</option><option value="32221">Compétences outre-mer</option><option value="32223">Déclassement</option><option value="32215">Incompatibilité des parlementaires</option><option value="32400">Obligations fiscales</option><option value="32216">Déchéance de parlementaires</option><option value="32222">Fins de non-recevoir</option><option value="32212">Avis de l'article 16</option><option value="32219">Élection présidentielle</option><option value="32214">Fonctionnement du Conseil constitutionnel</option><option value="32218">Référendum</option><option value="32403">Référendum d'initiative partagée</option><option value="32217">Divers élections</option><option value="32213">Autres textes et décisions</option></select>
</div>
<div class="block-facets block-facet-date-range-picker js-form-wrapper form-wrapper" id="periode" data-drupal-selector="edit-date-range"><div class="form-group js-form-wrapper form-wrapper" data-drupal-selector="edit-form-group" id="edit-form-group"><label data-drupal-selector="edit-label">Par période</label>
<div class="js-form-item form-item js-form-type-date form-type-date js-form-item-date-from form-item-date-from form-no-label">
<span class="clear-input hidden">X</span>
<input id="edit-date-from" name="date_from" data-drupal-selector="edit-date-from" type="date" value="" class="form-date">
</div>
<div class="js-form-item form-item js-form-type-date form-type-date js-form-item-date-to form-item-date-to form-no-label">
<span class="clear-input hidden">X</span>
<input id="edit-date-to" name="date_to" data-drupal-selector="edit-date-to" type="date" value="" class="form-date">
</div>
</div>
</div>
<input data-drupal-selector="edit-submit" type="submit" id="edit-submit" name="op" value="Lancer une recherche" class="button js-form-submit form-submit">
<input class="reset reload-page button js-form-submit form-submit" data-drupal-selector="edit-reset" type="submit" id="edit-reset" name="op" value="Réinitialiser">
<a href="/recherche-experte" class="lien-page-recherche" data-smarttag="[]" data-drupal-selector="edit-lien" id="edit-lien">Plus de critères de recherche</a><input autocomplete="off" data-drupal-selector="form-if1ifd6wffso28jvmkzvnsozeco7wqt14mauqf8sdi" type="hidden" name="form_build_id" value="form-_IF1iFd6WfFSo28JVMkZvnSozEcO7wQt14Mauqf8sdI">
<input data-drupal-selector="edit-cc-landing-recherche-decisions" type="hidden" name="form_id" value="cc_landing_recherche_decisions">
</form>
</div>
</header>
<div class="sidebar">
<h3>Outils et ressources</h3>
<div class="content">
<div class="item">
<div class="field field--name-field-date-analytique field--type-string field--label-above">
<div class="field__label">Les tables analytiques</div>
<div class="field__item">À jour au 1<sup>er</sup> septembre 2024</div>
</div>
<div class="field field--name-field-reference-analytique field--type-entity-reference field--label-hidden field__item"><article class="media media--type-file media--view-mode-ressource-decisions">
<a href="https://www.conseil-constitutionnel.fr/sites/default/files/2024-10/1958-202408_tables.pdf">Tables analytiques 1958-2024</a><div class="type-size"><span class="type">Pdf</span>
<span class="size">33.95 Mo</span>
</div>
</article>
</div>
<div class="field field--name-field-lien-analytique field--type-link field--label-hidden field__item"><a href="/les-decisions/tables-analytiques">En savoir plus</a></div>
</div>
<div class="item">
<div class="field field--name-field-lien-dispositions field--type-link field--label-above">
<div class="field__label">Recueil des dispositions validées</div>
<div class="field__item"><a href="/dispositions">Voir le recueil</a></div>
</div>
</div>
<div class="item">
<div class="field field--name-field-date-bilan field--type-string field--label-above">
<div class="field__label">Bilan statistique</div>
<div class="field__item">À jour au 30 juin 2025</div>
</div>
<div class="field field--name-field-lien-bilan field--type-link field--label-hidden field__item"><a href="/bilan-statistique">Voir le bilan</a></div>
</div>
<div class="item">
<div class="field field--name-field-lien-archive-seance field--type-link field--label-above">
<div class="field__label">Archives des comptes rendus de séances</div>
<div class="field__item"><a href="/les-decisions/archives-des-comptes-rendus-de-seances">En savoir plus</a></div>
</div>
</div>
</div>
</div>
<div class="wrapper-content">
<div class="wrapper-dernieres-decisions"><h2>Les dernières décisions</h2>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026908DC.htm" rel="bookmark" title="Décision&nbsp;n°&nbsp;2026-908 DC du 23 juillet 2026">
<div class="node__content">
<h3 class="title">Décision&nbsp;&nbsp;2026-908 DC du 23 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi organique relative au renforcement des juridictions criminelles</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026909DC.htm" rel="bookmark" title="Décision n° 2026-909 DC du 23 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-909 DC du 23 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi sur la justice criminelle et le respect des victimes</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Non conformité partielle - réserve</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026906DC.htm" rel="bookmark" title="Décision&nbsp;n°&nbsp;2026-906 DC du 23&nbsp;juillet&nbsp;2026">
<div class="node__content">
<h3 class="title">Décision&nbsp;&nbsp;2026-906 DC du 23&nbsp;juillet&nbsp;2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi visant à renforcer la sécurité, la rétention administrative et la prévention des risques dattentat</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité - réserve</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/20261213QPC.htm" rel="bookmark" title="Décision n° 2026-1213 QPC du 10 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-1213 QPC du 10 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Société Altaroc Partners et autres [Notification du droit de se taire à la personne à laquelle sont notifiés des griefs dans le cadre d'une procédure de sanction par lAutorité des marchés financiers]</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/20269LP.htm" rel="bookmark" title="Décision n°&nbsp;2026-9&nbsp;LP du 9 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n°&nbsp;2026-9&nbsp;LP du 9 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi du pays modifiant le code minier de la Nouvelle-Calédonie (partie législative)</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026326L.htm" rel="bookmark" title="Décision n° 2026-326 L du 9 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-326 L du 9 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Nature juridique de certaines dispositions de larticle&nbsp;L.&nbsp;122-8 du code de l’énergie</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Réglementaire</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/20261211QPC.htm" rel="bookmark" title="Décision n° 2026-1211 QPC du 3 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-1211 QPC du 3 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>M.&nbsp;Maurice&nbsp;D. [Interdiction de linscription au tableau de lordre des médecins, chirurgiens dentistes et sages-femmes inscrits ou enregistrés dans un autre État]</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/20261212QPC.htm" rel="bookmark" title="Décision n° 2026-1212 QPC du 3&nbsp;juillet&nbsp;2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-1212 QPC du 3&nbsp;juillet&nbsp;2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>M.&nbsp;Valeriu M. [Circonstances justifiant laggravation de la suspension prononcée par lAgence française de lutte contre le dopage]</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Non conformité totale</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026325L.htm" rel="bookmark" title="Décision n° 2026-325 L du 2 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-325 L du 2 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Nature juridique de certaines dispositions de larticle L.&nbsp;1422-3 du code du travail et de larticle L.&nbsp;131-3 du code de lentrée et du séjour des étrangers et du droit dasile</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Réglementaire</p>
</div>
</div>
</a>
</article>
<article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/20261210QPC.htm" rel="bookmark" title="Décision n° 2026-1210 QPC du 25 juin 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-1210 QPC du 25 juin 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Société Orange SA [Cumul de poursuites et de sanctions en cas de manquement aux règles encadrant la prospection directe par voie automatisée]</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Non conformité totale - effet différé - réserve transitoire</p>
</div>
</div>
</a>
</article>
</div>
<a href="/les-decisions" class="lien-toutes-decisions">Toutes les décisions</a>
</div></article><article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026908DC.htm" rel="bookmark" title="Décision&nbsp;n°&nbsp;2026-908 DC du 23 juillet 2026">
<div class="node__content">
<h3 class="title">Décision&nbsp;&nbsp;2026-908 DC du 23 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi organique relative au renforcement des juridictions criminelles</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité</p>
</div>
</div>
</a>
</article><article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026909DC.htm" rel="bookmark" title="Décision n° 2026-909 DC du 23 juillet 2026">
<div class="node__content">
<h3 class="title">Décision n° 2026-909 DC du 23 juillet 2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi sur la justice criminelle et le respect des victimes</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Non conformité partielle - réserve</p>
</div>
</div>
</a>
</article><article class="node node--type-decision node--promoted node--view-mode-synthetique">
<a href="/decision/2026/2026906DC.htm" rel="bookmark" title="Décision&nbsp;n°&nbsp;2026-906 DC du 23&nbsp;juillet&nbsp;2026">
<div class="node__content">
<h3 class="title">Décision&nbsp;&nbsp;2026-906 DC du 23&nbsp;juillet&nbsp;2026</h3>
<div class="field field--name-field-titre-complet field--type-string-long field--label-hidden field__item">
<p>Loi visant à renforcer la sécurité, la rétention administrative et la prévention des risques dattentat</p>
</div>
<div class="field field--name-field-solution-normalisee field--type-string field--label-hidden field__item">
<p>Conformité - réserve</p>
</div>
</div>
</a>
</article></body></html>
+32
View File
@@ -0,0 +1,32 @@
<html><body><a href="/dossier-legislatif/pjl25-457.html">Loi n° 2026-650 du 23 juillet 2026 relative au renforcement des juridictions criminelles
parue au JO n°171 du 24 juillet 2026</a><a href="/dossier-legislatif/pjl25-456.html">Loi n° 2026-651 du 23 juillet 2026 sur la justice criminelle et le respect des victimes
parue au JO n°171 du 24 juillet 2026</a><a href="/dossier-legislatif/ppl25-214.html">Loi n° 2026-630 du 13 juillet 2026 visant à assurer le droit de chaque enfant à être assisté d'un avocat dans le cadre d'une mesure d'assistance éducative et de protection de l'enfance
parue au JO n°163 du 14 juillet 2026</a><a href="/dossier-legislatif/ppl23-431.html">Loi n° 2026-602 du 8 juillet 2026 visant à réduire l'impact environnemental de l'industrie textile
parue au JO n°159 du 9 juillet 2026</a><a href="/dossier-legislatif/pjl25-283.html">Loi n° 2026-574 du 30 juin 2026 portant habilitation de l'assemblée de Martinique à fixer des règles applicables sur son territoire en application de l'article 73 de la Constitution en matière d'énergie, d'eau et d'assainissement
parue au JO n°152 du 1 juillet 2026</a><a href="/dossier-legislatif/ppl25-359.html">Loi n° 2026-554 du 29 juin 2026 visant à relancer les investissements dans le secteur de l'hydroélectricité pour contribuer à la transition énergétique
parue au JO n°151 du 30 juin 2026</a><a href="/dossier-legislatif/ppl25-320.html">Loi n° 2026-555 du 29 juin 2026 visant à réparer les préjudices causés par la transplantation de mineurs de La Réunion en France hexagonale de 1962 à 1984
parue au JO n°151 du 30 juin 2026</a><a href="/dossier-legislatif/ppl25-273.html">Loi n° 2026-553 du 29 juin 2026 visant à améliorer l'accès au logement des travailleurs des services publics
parue au JO n°151 du 30 juin 2026</a><a href="/dossier-legislatif/ppl24-008.html">Loi n° 2026-542 du 26 juin 2026 relative à la sortie des collections publiques de restes humains kali'nas et arawaks en vue de funérailles sur le territoire de la Guyane
parue au JO n°149 du 27 juin 2026</a><a href="/dossier-legislatif/pjl25-024.html">Loi n° 2026-534 du 25 juin 2026 relative à la lutte contre les fraudes sociales et fiscales
parue au JO n°148 du 26 juin 2026</a><a href="/dossier-legislatif/ppl23-483.html">Loi n° 2026-532 du 24 juin 2026 visant à ouvrir le dispositif de réduction d'activité aux moniteurs de ski stagiaires
parue au JO n°147 du 25 juin 2026</a><a href="/dossier-legislatif/ppl24-180.html">Loi n° 2026-492 du 12 juin 2026 visant à améliorer la protection et l'accompagnement des parents d'enfants atteints d'un cancer, d'une maladie grave ou d'un handicap
parue au JO n°137 du 13 juin 2026</a><a href="/dossier-legislatif/ppl23-373.html">Loi n° 2026-491 du 12 juin 2026 visant à reconnaître la responsabilité de l'État et à indemniser les victimes du chlordécone
parue au JO n°137 du 13 juin 2026</a><a href="/dossier-legislatif/pjl25-470.html">Loi n° 2026-470 du 11 juin 2026 portant transposition de l'avenant n° 3 du 25 février 2026 au protocole d'accord du 10 novembre 2023 relatif à l'assurance chômage
parue au JO n°136 du 12 juin 2026</a><a href="/dossier-legislatif/ppl25-213.html">Loi n° 2026-442 du 4 juin 2026 visant à étendre à toutes les communes la compensation financière prévue pour les communes de plus de 3 500 habitants pour l'exercice de l'ensemble des compétences du service public de la petite enfance
parue au JO n°130 du 5 juin 2026</a><a href="/dossier-legislatif/ppl24-475.html">Loi n° 2026-441 du 4 juin 2026 portant pérennisation du contrat de professionnalisation expérimental
parue au JO n°130 du 5 juin 2026</a><a href="/dossier-legislatif/ppl24-636.html">Loi n° 2026-410 du 28 mai 2026 organique portant régularisation des natifs dans le corps électoral pour les élections au congrès et aux assemblées de province de Nouvelle-Calédonie
parue au JO n°124 du 29 mai 2026</a><a href="/dossier-legislatif/pjl23-550.html">Loi n° 2026-403 du 26 mai 2026 de simplification de la vie économique
parue au JO n°122 du 27 mai 2026</a><a href="/dossier-legislatif/ppl24-662.html">Loi n° 2026-404 du 26 mai 2026 visant à garantir l'égal accès de tous à l'accompagnement et aux soins palliatifs
parue au JO n°122 du 27 mai 2026</a><a href="/dossier-legislatif/ppl24-226.html">Loi n° 2026-381 du 19 mai 2026 visant à soutenir les collectivités territoriales dans la prévention et la gestion des inondations
parue au JO n°117 du 20 mai 2026</a><a href="/dossier-legislatif/ppl25-401.html">Loi n° 2026-373 du 15 mai 2026 facilitant l'exercice en France des médecins diplômés au Royaume-Uni ayant débuté leurs études avant le Brexit
parue au JO n°114 du 16 mai 2026</a><a href="/dossier-legislatif/pjl24-871.html">Loi n° 2026-351 du 9 mai 2026 relatif à la restitution de biens culturels ayant fait l'objet d'une appropriation illicite
parue au JO n°109 du 10 mai 2026</a><a href="/dossier-legislatif/ppl24-637.html">Loi n° 2026-350 du 9 mai 2026 visant à garantir le droit de visite des parlementaires et des bâtonniers dans les lieux de privation de liberté
parue au JO n°109 du 10 mai 2026</a><a href="/dossier-legislatif/ppl25-187.html">Loi n° 2026-307 du 23 avril 2026 visant à instaurer une procédure simplifiée de recouvrement des créances commerciales incontestées
parue au JO n°97 du 24 avril 2026</a><a href="/dossier-legislatif/ppl24-415.html">Loi n° 2026-248 du 7 avril 2026 visant à simplifier la sortie de l'indivision et la gestion des successions vacantes
parue au JO n°83 du 8 avril 2026</a><a href="/dossier-legislatif/ppl25-328.html">Loi n° 2026-249 du 7 avril 2026 visant à permettre le remboursement des frais d'expertise comptable aux candidats
parue au JO n°83 du 8 avril 2026</a><a href="/dossier-legislatif/ppl24-413.html">Loi n° 2026-247 du 7 avril 2026 relative aux missions des professionnels de santé, vétérinaires, psychothérapeutes et psychologues des services d'incendie et de secours
parue au JO n°83 du 8 avril 2026</a><a href="/dossier-legislatif/pjl24-630.html">Loi n° 2026-201 du 20 mars 2026 relative à l'organisation des jeux Olympiques et Paralympiques de 2030
parue au JO n°69 du 21 mars 2026</a><a href="/dossier-legislatif/ppl23-569.html">Loi n° 2026-122 du 23 février 2026 relative à la confidentialité des consultations des juristes d'entreprise
parue au JO n°48 du 25 février 2026</a><a href="/dossier-legislatif/pjlf2026.html">Loi n° 2026-103 du 19 février 2026 de finances pour 2026
parue au JO n°43 du 20 février 2026</a><a href="/dossier-legislatif/ppl24-223.html">Loi n° 2026-6 du 7 janvier 2026 organique tendant à modifier le II de l'article 43 de la loi organique n° 2004-192 du 27 février 2004 portant statut d'autonomie de la Polynésie française
parue au JO n°6 du 8 janvier 2026</a></body></html>
+4 -1
View File
@@ -212,7 +212,10 @@ def test_une_fiche_a_timeline_et_sources(base_complete) -> None:
def test_les_deductions_sont_signalees(base_complete) -> None:
"""Toute valeur déduite porte un motif de vérification lisible."""
for ligne in base_complete.execute("SELECT id, motif_verification FROM textes WHERE a_verifier = 1"):
lignes = base_complete.execute(
"SELECT id, motif_verification FROM textes WHERE a_verifier = 1"
)
for ligne in lignes:
assert ligne["motif_verification"], f"{ligne['id']} marqué à vérifier sans motif"
+299
View File
@@ -0,0 +1,299 @@
"""Collecteurs et rapprochement.
Les parseurs sont testés sur des fixtures HTML figées, capturées le 25 juillet
2026 sur les sites officiels : aucun test ne dépend du réseau. Une fixture qui
cesse de passer signale un changement de structure du site — c'est le but.
"""
from __future__ import annotations
from datetime import date
from pathlib import Path
import pytest
from selectolax.parser import HTMLParser
from pipeline.collecteurs.conseil_constitutionnel import (
_index_des_colonnes,
_lire_decision_rendue,
_lire_ligne_instance,
)
from pipeline.collecteurs.http import ClientHttp, _cle_de_cache
from pipeline.collecteurs.legifrance import CollecteurLegifrance, _lire_titre
from pipeline.collecteurs.senat import CollecteurSenat
from pipeline.modeles import ResultatCC, Statut, TypeTexte
from pipeline.rapprochement import Issue, normaliser, rapprocher, similarite
FIXTURES = Path(__file__).parent / "fixtures"
# ─────────────────────────────────────────────────────────────────────────────
# Conseil constitutionnel
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def affaires_dc() -> list:
tableau = HTMLParser(
(FIXTURES / "cc_affaires_dc.html").read_text(encoding="utf-8")
).css_first("table")
colonnes = _index_des_colonnes(tableau)
return [
affaire
for ligne in tableau.css("tbody tr")
if (affaire := _lire_ligne_instance(ligne, colonnes))
]
def test_les_sept_affaires_dc_en_instance(affaires_dc) -> None:
"""Le corpus en annonçait sept ; le registre officiel les confirme."""
numeros = {a.numero_affaire for a in affaires_dc}
assert numeros == {
"2026-907 DC",
"2026-910 DC",
"2026-911 DC",
"2026-912 DC",
"2026-913 DC",
"2026-914 DC",
"2026-915 DC",
}
def test_saisine_de_la_loi_riposte(affaires_dc) -> None:
riposte = next(a for a in affaires_dc if a.numero_affaire == "2026-915 DC")
assert riposte.date_saisine == date(2026, 7, 24)
assert riposte.resultat is ResultatCC.EN_INSTANCE
assert "soixante députés" in (riposte.saisissants or "")
def test_la_programmation_militaire_recoit_son_numero(affaires_dc) -> None:
"""Le rapport la citait sans numéro d'affaire ; la collecte le fournit."""
lpm = next(a for a in affaires_dc if a.numero_affaire == "2026-907 DC")
assert lpm.date_saisine == date(2026, 7, 6)
assert "programmation militaire" in (lpm.resume or "").lower()
def test_colonnes_lues_par_entete_et_non_par_position() -> None:
"""Les tableaux DC et QPC n'ont pas les mêmes colonnes."""
tableau = HTMLParser(
(FIXTURES / "cc_affaires_dc.html").read_text(encoding="utf-8")
).css_first("table")
colonnes = _index_des_colonnes(tableau)
assert colonnes["saisissants"] != colonnes["date_saisine"]
assert set(colonnes) >= {"affaire", "disposition", "saisissants", "date_saisine"}
@pytest.mark.parametrize(
("intitule", "attendu"),
[
("2026-908 DC du 23 juillet 2026 Loi organique … Conformité", ResultatCC.CONFORME),
(
"2026-906 DC du 23 juillet 2026 Loi … Conformité - réserve",
ResultatCC.CONFORME_AVEC_RESERVES,
),
(
"2026-909 DC du 23 juillet 2026 Loi … Non conformité partielle",
ResultatCC.NON_CONFORMITE_PARTIELLE,
),
],
)
def test_lecture_du_resultat_d_une_decision(intitule: str, attendu: ResultatCC) -> None:
"""« Conformité » ne contient pas « conforme » : l'ordre des motifs compte."""
decision = _lire_decision_rendue(f"Décision n° {intitule}", None)
assert decision is not None
assert decision.resultat is attendu
assert decision.date_decision == date(2026, 7, 23)
def test_decisions_rendues_de_la_fixture() -> None:
document = HTMLParser((FIXTURES / "cc_decisions.html").read_text(encoding="utf-8"))
decisions = [
d
for article in document.css("article")
if (d := _lire_decision_rendue(" ".join(article.text(separator=" ").split()), None))
]
numeros = {d.numero_affaire for d in decisions}
assert {"2026-906 DC", "2026-908 DC", "2026-909 DC"} <= numeros
# ─────────────────────────────────────────────────────────────────────────────
# Sénat
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def lois_senat() -> list:
document = HTMLParser(
(FIXTURES / "senat_lois_promulguees.html").read_text(encoding="utf-8")
)
collecteur = CollecteurSenat(ClientHttp(hors_ligne=True))
textes = []
for ancre in document.css("a"):
libelle = " ".join(ancre.text(separator=" ", strip=True).split())
if texte := collecteur._lire_intitule(libelle, ancre.attributes.get("href")):
textes.append(texte)
return textes
def test_senat_releve_les_lois_de_la_fenetre(lois_senat) -> None:
numeros = {t.numero_officiel for t in lois_senat}
# Quatre lois de la fenêtre de veille, aux dates connues du corpus.
assert {"2026-651", "2026-650", "2026-630", "2026-602"} <= numeros
def test_senat_lit_dates_et_nature(lois_senat) -> None:
par_numero = {t.numero_officiel: t for t in lois_senat}
justice = par_numero["2026-651"]
assert justice.date_promulgation == date(2026, 7, 23)
assert justice.statut is Statut.PROMULGUEE
assert justice.type is TypeTexte.LOI
def test_les_deux_sources_ne_disent_pas_la_meme_chose_du_caractere_organique(
lois_senat,
) -> None:
"""Asymétrie réelle entre les sources, à ne pas corriger en silence.
Légifrance intitule la loi n° 2026-650 « LOI organique n° 2026-650 … » ;
la liste du Sénat écrit « Loi n° 2026-650 … » sans la qualifier. Le
collecteur rapporte ce que sa source dit — c'est le rôle du rapprochement
de ne pas laisser la source la plus pauvre écraser la plus riche. Le run de
mise à jour ne compare d'ailleurs pas le champ `type`.
"""
organique = par_numero_senat(lois_senat)["2026-650"]
assert organique.type is TypeTexte.LOI, (
"le Sénat ne qualifie pas cette loi d'organique : le collecteur ne doit "
"pas le deviner à sa place"
)
def par_numero_senat(lois: list) -> dict:
return {t.numero_officiel: t for t in lois}
def test_senat_ne_confond_pas_l_objet_avec_l_etat(lois_senat) -> None:
"""Le libellé se termine par « parue » : ce n'est pas l'objet de la loi."""
for texte in lois_senat:
assert not texte.titre.rstrip().endswith("parue")
assert "en cours" not in texte.titre
def test_senat_produit_une_source_primaire(lois_senat) -> None:
for texte in lois_senat:
assert texte.sources
assert texte.sources[0].tier == "T1"
assert texte.sources[0].url.startswith("https://www.senat.fr")
# ─────────────────────────────────────────────────────────────────────────────
# Légifrance
# ─────────────────────────────────────────────────────────────────────────────
def test_legifrance_indisponible_sans_identifiants(monkeypatch) -> None:
monkeypatch.delenv("LEGIFRANCE_CLIENT_ID", raising=False)
collecteur = CollecteurLegifrance(ClientHttp(hors_ligne=True))
disponible, motif = collecteur.est_disponible()
assert disponible is False
assert "repli" in (motif or "")
def test_legifrance_lit_un_resultat_de_recherche() -> None:
titre = {
"id": "LEGITEXT000054249605_14-06-2026",
"cid": "JORFTEXT000054245243",
"title": "LOI n° 2026-491 du 12 juin 2026 visant à reconnaître la responsabilité de l'Etat",
}
texte = _lire_titre(titre, "2026-491")
assert texte is not None
assert texte.date_promulgation == date(2026, 6, 12)
assert texte.source_url.endswith("JORFTEXT000054245243")
assert texte.sources[0].tier == "T1"
def test_legifrance_ecarte_un_resultat_hors_sujet() -> None:
titre = {"cid": "X", "title": "LOI n° 2026-999 du 1er janvier 2026 sans rapport"}
assert _lire_titre(titre, "2026-491") is None
def test_legifrance_deduit_le_caractere_organique() -> None:
titre = {
"cid": "JORF1",
"title": "LOI organique n° 2026-650 du 23 juillet 2026 relative aux juridictions",
}
texte = _lire_titre(titre, "2026-650")
assert texte.type is TypeTexte.LOI_ORGANIQUE
# ─────────────────────────────────────────────────────────────────────────────
# Client HTTP
# ─────────────────────────────────────────────────────────────────────────────
def test_le_cache_distingue_les_corps_de_requete() -> None:
a = _cle_de_cache("POST", "https://x.fr/search", {"q": "2026-491"}, None)
b = _cle_de_cache("POST", "https://x.fr/search", {"q": "2026-403"}, None)
assert a != b
def test_mode_hors_ligne_ne_touche_pas_au_reseau(tmp_path) -> None:
client = ClientHttp(cache=tmp_path, hors_ligne=True)
reponse = client.get("https://exemple.invalide/rien")
assert reponse.code == 0
assert not reponse.a_reussi
def test_le_cache_est_relu(tmp_path) -> None:
client = ClientHttp(cache=tmp_path, hors_ligne=True)
cle = _cle_de_cache("GET", "https://x.fr/a", None, None)
client._ecrire_cache(cle, "contenu")
reponse = client.get("https://x.fr/a")
assert reponse.texte == "contenu"
assert reponse.depuis_le_cache
# ─────────────────────────────────────────────────────────────────────────────
# Rapprochement
# ─────────────────────────────────────────────────────────────────────────────
def test_normalisation_retire_le_vocabulaire_passe_partout() -> None:
assert "loi" not in normaliser("LOI n° 2026-491 relative au chlordécone")
assert "chlordecone" in normaliser("LOI n° 2026-491 relative au chlordécone")
def test_similarite_reconnait_un_titre_court_dans_un_titre_long() -> None:
court = "Loi Riposte ordre public sécurité tranquillité"
long = (
"Loi visant à offrir des réponses immédiates aux phénomènes troublant "
"l'ordre public, la sécurité et la tranquillité de nos concitoyens"
)
assert similarite(court, long) >= 0.72
def test_le_numero_officiel_prime_sur_le_titre() -> None:
correspondance = rapprocher(
titre="Un intitulé totalement différent",
numero_officiel="2026-491",
candidats=[("loi-2026-491", "Chlordécone : responsabilité de l'État", "2026-491")],
)
assert correspondance.issue is Issue.RAPPROCHE
assert correspondance.texte_id == "loi-2026-491"
assert correspondance.score == 1.0
def test_un_texte_inconnu_est_declare_nouveau() -> None:
correspondance = rapprocher(
titre="Loi sur les moniteurs de ski stagiaires en zone de montagne",
numero_officiel=None,
candidats=[("loi-2026-491", "Chlordécone : responsabilité de l'État", "2026-491")],
)
assert correspondance.issue is Issue.NOUVEAU
def test_une_correspondance_douteuse_est_signalee_pas_fusionnee() -> None:
"""Entre les deux seuils, le pipeline s'abstient et demande un humain."""
correspondance = rapprocher(
titre="Loi relative à la protection des enfants et des mineurs",
numero_officiel=None,
candidats=[("pjl-protection-enfants", "PJL relatif à la protection des enfants", None)],
)
assert correspondance.issue in (Issue.RAPPROCHE, Issue.A_SIGNALER)
if correspondance.issue is Issue.A_SIGNALER:
assert "vérification humaine" in correspondance.motif
def test_aucun_candidat() -> None:
correspondance = rapprocher(titre="X", numero_officiel="2026-1", candidats=[])
assert correspondance.issue is Issue.NOUVEAU
+311
View File
@@ -0,0 +1,311 @@
"""Run de veille : détection des écarts, classification, export.
Aucun test ne va sur le réseau : les collectes sont fabriquées à la main, ce
qui permet de vérifier exactement quels écarts le pipeline doit voir — et
surtout lesquels il ne doit pas inventer.
"""
from __future__ import annotations
import json
import sqlite3
from datetime import date
import pytest
from pipeline import db
from pipeline.collecteurs.base import ResultatCollecte, TexteCollecte
from pipeline.modeles import (
Confiance,
DecisionCC,
Evenement,
Pertinence,
ResultatCC,
Source,
Statut,
Texte,
TypeEtape,
TypeTexte,
)
from pipeline.update import appliquer, classer_nouveau, comparer, exporter_json
@pytest.fixture
def base(tmp_path) -> sqlite3.Connection:
"""Base minimale : une loi promulguée et une loi en attente de décision."""
cx = db.initialiser(tmp_path / "veille.db")
db.enregistrer_textes(
cx,
[
Texte(
id="loi-2026-491",
numero_officiel="2026-491",
type=TypeTexte.LOI,
titre_court="Chlordécone : responsabilité de l'État",
statut=Statut.PROMULGUEE,
date_promulgation=date(2026, 6, 12),
guadeloupe_pertinence=Pertinence.FORTE,
sources=[
Source(
url="https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000054245243",
editeur="Légifrance",
)
],
evenements=[
Evenement(
date_evenement=date(2026, 6, 12),
type_etape=TypeEtape.PROMULGATION,
description="Promulgation",
)
],
),
Texte(
id="loi-riposte",
type=TypeTexte.LOI,
titre_court="Loi « Riposte » (ordre public, sécurité, tranquillité)",
statut=Statut.ADOPTEE_NON_PROMULGUEE,
date_adoption=date(2026, 7, 21),
sources=[Source(url="https://www.assemblee-nationale.fr/dyn/17/textes/l17t0340")],
decisions_cc=[
DecisionCC(
numero_affaire="2026-915 DC",
date_saisine=date(2026, 7, 24),
resultat=ResultatCC.EN_INSTANCE,
)
],
),
],
)
return cx
def _collecte(**kwargs) -> ResultatCollecte:
return ResultatCollecte(collecteur="essai", **kwargs)
# ─────────────────────────────────────────────────────────────────────────────
# Détection des écarts
# ─────────────────────────────────────────────────────────────────────────────
def test_aucun_ecart_quand_la_source_confirme_la_base(base) -> None:
"""Le cas normal : la veille ne doit rien signaler quand rien ne bouge."""
collecte = _collecte(
textes=[
TexteCollecte(
titre="LOI n° 2026-491 du 12 juin 2026 chlordécone",
source_url="https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000054245243",
collecteur="essai",
numero_officiel="2026-491",
statut=Statut.PROMULGUEE,
date_promulgation=date(2026, 6, 12),
)
]
)
rapport = comparer(base, [collecte])
assert rapport.ecarts == []
def test_un_texte_inconnu_est_un_ajout(base) -> None:
collecte = _collecte(
textes=[
TexteCollecte(
titre="LOI n° 2026-700 du 1er septembre 2026 relative aux transports scolaires",
source_url="https://www.legifrance.gouv.fr/jorf/id/X",
collecteur="essai",
numero_officiel="2026-700",
statut=Statut.PROMULGUEE,
date_promulgation=date(2026, 9, 1),
)
]
)
rapport = comparer(base, [collecte])
assert rapport.ajouts == 1
assert rapport.ecarts[0].nature == "ajout"
def test_un_changement_de_statut_est_detecte(base) -> None:
"""La promulgation de la loi « Riposte » : l'événement que la veille attend."""
collecte = _collecte(
textes=[
TexteCollecte(
titre="Loi « Riposte » (ordre public, sécurité, tranquillité)",
source_url="https://www.legifrance.gouv.fr/jorf/id/Y",
collecteur="essai",
statut=Statut.PROMULGUEE,
date_promulgation=date(2026, 8, 26),
)
]
)
rapport = comparer(base, [collecte])
statuts = [e for e in rapport.ecarts if e.nature == "statut"]
assert len(statuts) == 1
assert statuts[0].texte_id == "loi-riposte"
assert statuts[0].ancienne_valeur == "adoptee_non_promulguee"
assert statuts[0].nouvelle_valeur == "promulguee"
def test_une_decision_rendue_est_detectee(base) -> None:
collecte = _collecte(
decisions_cc=[
DecisionCC(
numero_affaire="2026-915 DC",
date_decision=date(2026, 8, 24),
resultat=ResultatCC.NON_CONFORMITE_PARTIELLE,
)
]
)
rapport = comparer(base, [collecte])
natures = {e.champ for e in rapport.ecarts if e.nature == "decision_cc"}
assert any("date_decision" in (c or "") for c in natures)
assert any("resultat" in (c or "") for c in natures)
def test_une_affaire_inconnue_est_signalee(base) -> None:
"""Cas réel : le corpus ignorait le numéro 2026-907 DC de la LPM."""
collecte = _collecte(
decisions_cc=[
DecisionCC(
numero_affaire="2026-907 DC",
date_saisine=date(2026, 7, 6),
resume="Loi actualisant la programmation militaire",
)
]
)
rapport = comparer(base, [collecte])
assert any("affaire inconnue" in e.description for e in rapport.ecarts)
def test_une_affaire_en_instance_deja_connue_ne_produit_rien(base) -> None:
collecte = _collecte(
decisions_cc=[
DecisionCC(
numero_affaire="2026-915 DC",
date_saisine=date(2026, 7, 24),
resultat=ResultatCC.EN_INSTANCE,
)
]
)
assert comparer(base, [collecte]).ecarts == []
# ─────────────────────────────────────────────────────────────────────────────
# Écriture
# ─────────────────────────────────────────────────────────────────────────────
def test_le_dry_run_n_ecrit_rien(base) -> None:
"""`comparer` ne doit jamais toucher à la base."""
avant = db.statistiques(base)
collecte = _collecte(
textes=[
TexteCollecte(
titre="Loi « Riposte » (ordre public, sécurité, tranquillité)",
source_url="https://x.fr/y",
collecteur="essai",
statut=Statut.PROMULGUEE,
)
]
)
comparer(base, [collecte])
assert db.statistiques(base) == avant
def test_appliquer_ecrit_le_changement_et_le_journalise(base) -> None:
collecte = _collecte(
textes=[
TexteCollecte(
titre="Loi « Riposte » (ordre public, sécurité, tranquillité)",
source_url="https://x.fr/y",
collecteur="essai",
statut=Statut.PROMULGUEE,
)
]
)
rapport = comparer(base, [collecte])
run_id = db.ouvrir_run(base, "update")
appliquer(base, rapport, run_id)
statut = base.execute("SELECT statut FROM textes WHERE id = 'loi-riposte'").fetchone()[0]
assert statut == "promulguee"
journal = base.execute(
"SELECT nature, champ, nouvelle_valeur FROM veille_changements WHERE run_id = ?",
(run_id,),
).fetchall()
assert any(
ligne["champ"] == "statut" and ligne["nouvelle_valeur"] == "promulguee"
for ligne in journal
)
# ─────────────────────────────────────────────────────────────────────────────
# Classification d'un texte nouveau
# ─────────────────────────────────────────────────────────────────────────────
def test_un_texte_collecte_est_marque_comme_automatique() -> None:
"""§5.3 : la machine propose avec une confiance basse, l'humain valide."""
texte = classer_nouveau(
TexteCollecte(
titre="LOI n° 2026-700 du 1er septembre 2026 relative à l'eau en Guadeloupe",
source_url="https://www.legifrance.gouv.fr/jorf/id/Z",
collecteur="legifrance",
numero_officiel="2026-700",
statut=Statut.PROMULGUEE,
date_promulgation=date(2026, 9, 1),
)
)
assert texte.id == "loi-2026-700"
assert texte.confiance is Confiance.LOW
assert texte.a_verifier is True
assert "déduits" in (texte.motif_verification or "")
assert texte.guadeloupe_pertinence is Pertinence.FORTE # « Guadeloupe » dans l'intitulé
def test_un_texte_sans_numero_recoit_un_identifiant_lisible() -> None:
texte = classer_nouveau(
TexteCollecte(
titre="Proposition de loi relative aux sargasses en Martinique",
source_url="https://www.senat.fr/x",
collecteur="senat",
statut=Statut.NAVETTE,
)
)
assert texte.id and " " not in texte.id
assert "sargasses" in texte.id
# ─────────────────────────────────────────────────────────────────────────────
# Export statique
# ─────────────────────────────────────────────────────────────────────────────
def test_export_json(base, tmp_path, monkeypatch) -> None:
from pipeline import chemins
destination = tmp_path / "textes.json"
monkeypatch.setattr(chemins, "DUMP_JSON", destination)
total = exporter_json(base)
assert total == 2
charge = json.loads(destination.read_text(encoding="utf-8"))
assert charge["date_arrete_corpus"] == "2026-07-25"
identifiants = {t["id"] for t in charge["textes"]}
assert identifiants == {"loi-2026-491", "loi-riposte"}
chlordecone = next(t for t in charge["textes"] if t["id"] == "loi-2026-491")
assert chlordecone["sources"][0]["url"].startswith("https://www.legifrance.gouv.fr")
assert chlordecone["evenements"][0]["type_etape"] == "promulgation"
riposte = next(t for t in charge["textes"] if t["id"] == "loi-riposte")
assert riposte["decisions_cc"][0]["numero_affaire"] == "2026-915 DC"
def test_rapport_lisible(base) -> None:
rapport = comparer(base, [_collecte()])
rapport.mode = "dry-run"
texte = rapport.en_texte()
assert "Rapport de veille" in texte
assert "conforme aux sources officielles" in texte