Files
Cyber MawonajandClaude Opus 5 c28243cf87 Phase 3 : sources, citations et seed complet — 368 sources, 0 texte orphelin
Parseurs :
- blocs_claim : les quatre dialectes des rapports de dimension. Le cahier des
  charges en postulait un seul ; sans les quatre, dim05 à dim12 étaient perdus,
  soit les dimensions associations, entreprises, migration et calendrier.
- notes_bas_page : 258 notes, dont 104 sans URL. Les identifiants JORFTEXT
  donnent lieu à une URL canonique reconstruite (schéma fixe, rien d'inventé) ;
  les autres restent sans lien plutôt que d'être devinées.
- bibliographie : 481 références, toutes liées. Indexées par numéro de loi,
  par affaire DC et par cote de document parlementaire — cette dernière étant
  la seule identité d'un texte déposé et non examiné.
- chapitres : rattachement phrase par phrase, résumés repris tels quels.

Résolution des citations : 100 % des 332 marqueurs du rapport, 93 % avec un
lien vérifiable. Il a fallu six conventions de numérotation, une par famille de
dimension (marqueur de fin, en-tête de claim, section, rang, note, section.rang).

Trois pièges corrigés, tous couverts par des tests :
- « décision n° 2026-903 DC » lu comme une loi, ce qui volait à la loi 2026-403
  les phrases — et donc les sources — qui parlaient d'elle
- retrait des marqueurs laissant « … dans les DROM . » ; corrigé à la source
  sans toucher aux espaces avant « ; » et « » » qu'exige le français
- « programmation militaire 2024-2030 » lu comme la loi n° 2024-2030

make seed : 52 textes, 368 sources, 83 événements, 12 affaires CC, 7 analyses.
Zéro texte sans source URL (critère §8.2). Les 2 textes sans résumé et les 17
pertinences déduites sont marqués « à vérifier » avec leur motif.

99 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:43:03 -04:00

92 lines
3.1 KiB
Python

"""Lecture des analyses transversales (`research/lois-2026_insight.md`).
Sept analyses, chacune au format ::
## Insight 3 — Le 1er septembre 2026 : « big bang » silencieux …
**Dérivé de :** dim08, dim09, dim02, dim12, dim03.
<corps>
**Implications :** …
**Confiance : high.**
Elles alimentent l'encart « point de veille » du tableau de bord. Elles ne
portent aucune source primaire propre — la table de correspondance de la
bibliographie le dit — et ne sont donc jamais présentées comme des faits
sourcés, mais comme des lectures d'ensemble.
"""
from __future__ import annotations
import re
from pipeline.journal import logger
from pipeline.modeles import Insight
from pipeline.parseurs.markdown import decouper_sections, nettoyer
log = logger("parseur.analyses")
_TITRE = re.compile(r"^Insight\s+(\d+)\s*[—–-]\s*(.+)$", re.IGNORECASE)
_DERIVE = re.compile(r"\*\*Dérivé de\s*:\*\*\s*(.+)", re.IGNORECASE)
_IMPLICATIONS = re.compile(r"\*\*Implications\s*:\*\*\s*(.+)", re.IGNORECASE)
_CONFIANCE = re.compile(r"\*\*Confiance\s*:\s*([a-zé-]+)", re.IGNORECASE)
_DIMENSION = re.compile(r"\b(dim\d{2}|phase\s?5)\b", re.IGNORECASE)
def parser(markdown: str) -> list[Insight]:
"""Extrait les analyses transversales du fichier d'insights."""
analyses: list[Insight] = []
for section in decouper_sections(markdown, niveau_max=2):
entete = _TITRE.match(section.titre.strip())
if not entete:
continue
corps_brut = section.corps
implications = None
if trouve := _IMPLICATIONS.search(corps_brut):
implications = nettoyer(trouve.group(1))
confiance = "medium"
if trouve := _CONFIANCE.search(corps_brut):
brut = trouve.group(1).lower()
confiance = {"high": "high", "élevée": "high", "medium": "medium", "low": "low"}.get(
brut, "medium"
)
derive_de: list[str] = []
if trouve := _DERIVE.search(corps_brut):
derive_de = [
d.lower().replace(" ", "") for d in _DIMENSION.findall(trouve.group(1))
]
corps = _corps_utile(corps_brut)
analyses.append(
Insight(
numero=int(entete.group(1)),
titre=nettoyer(entete.group(2)),
corps=corps,
implications=implications,
confiance=confiance,
derive_de=derive_de,
fichier_origine="research/lois-2026_insight.md",
)
)
log.info("analyses transversales lues", analyses=len(analyses))
return analyses
def _corps_utile(corps: str) -> str:
"""Corps de l'analyse, sans les lignes de métadonnées."""
lignes: list[str] = []
for ligne in corps.splitlines():
depouillee = ligne.strip()
if not depouillee:
continue
if _DERIVE.match(depouillee) or _IMPLICATIONS.match(depouillee):
continue
if _CONFIANCE.match(depouillee):
continue
lignes.append(nettoyer(depouillee))
return " ".join(lignes).strip()