# L'Orchestrateur d'Ingestion

L'ingestion est une CLI Node.js (`services/ingestion/src/cli.ts`, bâtie sur commander) qui télécharge les sources publiques, les transforme et les écrit dans PostgreSQL, puis relie les entités entre elles, calcule les statistiques et génère les résumés IA. Il n'y a ni file de messages ni worker permanent : chaque commande s'exécute puis rend la main, avec une concurrence bornée par `p-limit`.

---

## ⏱️ Deux passages planifiés en production

| Service Railway | Planification (UTC) | Heure de Paris | Commande |
| :--- | :--- | :--- | :--- |
| Ingestion nightly | `0 3 * * *` | 5 h en heure d'été, 4 h en heure d'hiver | `node --max-old-space-size=6144 services/ingestion/dist/cli.js smart-sync --all` |
| Ingestion intraday | `0 6-21/2 * * *` | toutes les deux heures, de 8 h à 22 h en été (de 7 h à 21 h en hiver) | `node services/ingestion/dist/cli.js sync-intraday` |

Railway exprime ses crons en UTC : l'heure locale glisse donc d'une heure au changement d'heure. Les deux services sont construits depuis `services/ingestion/Dockerfile`, seul à installer `pdftotext`.

Le passage **intraday** ne rafraîchit que ce qui change dans la journée : l'agenda du Sénat (de J-2 à J+2), les vidéos du Sénat (séances et commissions) et celles de l'Assemblée. Il ne touche ni aux scrutins, ni aux amendements, ni aux dossiers, ni aux statistiques.

---

## 🔄 Le batch nocturne : `smart-sync --all`

<Mermaid
  title="Déroulé du batch nocturne"
  chart={`flowchart TD
    A[Cron Railway] --> B[Événements institutionnels<br/>liste curée dans le code]
    B --> C{Pour chacune des 23 sources,<br/>dans l'ordre}
    C -->|ETag / Last-Modified inchangés| D[Ignorée<br/>sync_logs : skipped]
    C -->|changée, ou source « toujours »| E[Synchronisée<br/>sync_logs : started puis completed / failed]
    D --> C
    E --> C
    C -->|fin de la boucle| F[Rattachements entre entités]
    F --> G[Statistiques]
    G --> H[Sujets, liens, compteurs, VACUUM ANALYZE]
    H --> I[Déclarations HATVP, statut des sujets]
    I --> J[Enrichissement IA]
    J --> K[Pause de 120 s, puis reconstruction<br/>du cache de la page d'accueil]
`}
/>

### 1. Événements institutionnels

La liste des repères de l'agenda (élections, bornes de session, suspensions, échéances budgétaires) est écrite en dur dans `workers/evenements.ts` et poussée par un upsert idempotent sur son `slug`. Aucun appel réseau.

### 2. Les sources, dans cet ordre

L'ordre suit les dépendances : les commissions avant les députés (dont les mandats pointent vers les commissions), les parlementaires avant les votes, les anciens sénateurs avant les scrutins (les votes historiques ont besoin de l'annuaire), les gouvernements après les parlementaires (un ministre est rattaché à sa fiche), les candidatures en dernier (le rattachement d'un candidat à sa fiche lit le corpus).

1. `assemblee_nationale:commissions`
2. `assemblee_nationale:deputes`
3. `senat:senateurs`
4. `senat:senateurs_histo`
5. `senat:fonctions`
6. `assemblee_nationale:gouvernements`
7. `assemblee_nationale:scrutins`
8. `senat:scrutins`
9. `assemblee_nationale:amendements`
10. `senat:amendements`
11. `assemblee_nationale:dossiers`
12. `senat:dossiers`
13. `assemblee_nationale:interventions`
14. `senat:interventions`
15. `hatvp:lobbyistes`
16. `assemblee_nationale:reunions`
17. `assemblee_nationale:seances_odj`
18. `senat:agenda`
19. `senat:reunions`
20. `senat:dossier_commissions`
21. `senat:videos`
22. `assemblee_nationale:videos`
23. `interieur:candidatures`

Le détail de chaque source est sur la page [Sources de données](/donnees/sources-de-donnees). Les résultats des élections et les mandats locaux des candidats ne sont dans aucun des deux batchs : ils se lancent à la main (voir [Commandes CLI](/donnees/commandes-cli)).

**Fraîcheur.** Avant de télécharger, le batch compare l'`ETag` et le `Last-Modified` de la ressource à ceux enregistrés dans `source_states`. S'ils sont identiques, la source est ignorée (sauf avec `--force`). Les sources sans en-tête exploitable sont marquées « toujours » et relues à chaque passage.

**Échecs.** Une source en échec est journalisée (`sync_logs.statut = 'failed'`) et n'arrête pas le batch : elle est retirée des sources « changées », ce qui empêche les étapes qui en dépendent de tourner sur une lecture ratée. Le processus ne sort en erreur que si au moins une source a échoué **et** qu'aucune n'a été synchronisée avec succès. Un échec partiel se lit dans le récapitulatif et dans les logs d'erreur ; rejouer un batch entier pour une seule source resolliciterait toutes les autres.

### 3. Rattachements

Chaque étape ne tourne que si une source concernée a changé, et son échec n'est pas bloquant.

| Condition | Étape |
| :--- | :--- |
| interventions ou scrutins | Seconde lecture des comptes rendus de l'Assemblée (sans effet si rien n'a bougé), puis rattachement des débats de l'Assemblée aux scrutins qu'ils précèdent (table `intervention_scrutin`), puis de ceux du Sénat |
| interventions ou amendements | Rattachement des prises de parole à leur dossier, par le numéro de texte en discussion |
| réunions | Débats de commission de l'Assemblée et avis de commission sur les amendements, pour les réunions des 60 derniers jours |
| amendements ou scrutins | Retrait des liens vers un amendement déposé après le vote, puis lecture des pages des scrutins encore sans amendement (Assemblée : 30 derniers jours ; Sénat : nouveaux scrutins) |
| dossiers ou scrutins | Scrutins → dossiers : retrait des liens vers un dossier d'une autre législature (AN) ou d'une procédure sans vote (rapport, mission d'information, 49.3 hors motion de censure), rattachement au dossier que la source désigne (`voteRefs`), puis, pour les scrutins restés sans dossier : rattachement Sénat, TF-IDF sur les titres, titre (Assemblée), numéro de texte, titre de la loi promulguée |
| amendements, scrutins ou dossiers | Scrutins ↔ amendements (table de jointure), puis dossier des amendements : retrait des dossiers d'une autre législature, dossier du texte (`texte_ref`), propagation par leurs scrutins et entre amendements d'un même texte ; correction des liens dont l'article contredit le libellé du scrutin ; alignement du dossier des scrutins sur celui de leurs amendements |
| amendements | Texte des articles des nouvelles versions de textes de l'Assemblée (`textes_articles`). Une version déjà ingérée n'est pas retéléchargée ; seuls les textes que la source ne sert pas, ou dont le gabarit n'est pas couvert (budgets), sont retentés |
| toujours | Index de la base LEGI (`legi_textes`, `legi_articles`) : les incréments publiés par la DILA depuis la dernière archive appliquée, quelques secondes. Ne charge jamais le dump global : un index vide ou décroché des archives publiées fait échouer l'étape, à relancer à la main avec `sync-index-legi`. Son échec apparaît dans le récapitulatif des sources en échec ; sa réussite ne compte pas comme un changement et ne déclenche donc pas les étapes suivantes |
| toujours | Renvois au droit en vigueur dans le dispositif des amendements (`amendements_renvois`) : amendements nouveaux, modifiés, ou déposés depuis moins de 45 jours (la DILA consolide une loi nouvelle avec quelques jours ou semaines de retard). Vient après les amendements, le texte des articles (qui donne le code modifié quand le dispositif ne le nomme pas) et l'index LEGI. Un échec est journalisé sans bloquer la suite ; `check-quality` le signale |
| scrutins | Classement de la nature des scrutins encore non classés (`nature_vote`) |

L'ordre compte : le rattachement scrutin → dossier précède le rattachement scrutin → amendement, qui s'appuie sur le dossier pour éviter les faux positifs. Le texte des articles vient après le rattachement aux dossiers, dont il recopie le `dossier_id`, et avant l'IA, qui met l'article dans le prompt des scrutins.

### 4. Étapes exécutées dès qu'au moins une source a changé

Avec `--all`, les événements institutionnels et les sources « toujours » comptent comme changés : **ces étapes tournent donc chaque nuit.**

1. **Statistiques** (sauf `--skip-stats`) : réalignement de `parlementaires.actif` sur « a un mandat en cours », statistiques des parlementaires, des groupes, alliances entre groupes, positions par thématique.
2. **Sujets** : génération incrémentale (regroupement des dossiers AN et Sénat), liens « construction » (documents officiels), liens « contexte » (vie-publique.fr et Wikipédia, en incrémental).
3. **Compteurs dénormalisés** des sujets (`dossier_count`, `scrutin_count`, `date_dernier_vote`).
4. **`VACUUM ANALYZE`** sur `scrutins`, `amendements`, `dossiers_legislatifs`, `parlementaires`, `votes`, `interventions`.
5. **Déclarations HATVP** des parlementaires.
6. **Statut des sujets**, recalculé à partir de l'état de leurs dossiers.
7. **Enrichissement IA**, en cascade : scrutins, dossiers, sujets, descriptions d'amendements par groupe, puis les fiches parlementaires en trois temps : les fiches encore absentes des parlementaires en exercice, jusqu'à 200 fiches devenues périmées (un fait qu'elles affirment a changé, ou le prompt a changé de version), et 25 fiches tirées au hasard parmi celles non rafraîchies depuis 25 jours, anciens parlementaires compris (ce que nos données ignorent, un ancien député devenu maire par exemple, ne périme aucune empreinte). Voir [Enrichissement IA](/donnees/enrichissement-ia).

### 5. Cache de la page d'accueil

En fin de batch, la CLI attend 120 s, puis appelle `POST /api/v1/homepage/warm` (invalidation) et `GET /api/v1/homepage` (reconstruction), authentifiée par `CLAIR_INTERNAL_SECRET` ; chaque appel est borné à 120 s. Sans ce secret, l'étape est sautée. Les autres entrées du cache Redis de l'API ne sont pas invalidées : elles expirent selon leur durée de vie (voir [Limites & Cache](/guide/api-reference/authentification-et-limites)).

---

## 🧾 Traçabilité

- `source_states` : une ligne par source, avec l'URL de la ressource, le dernier `ETag`, le dernier `Last-Modified`, la date de dernière synchronisation réussie et celle de dernière vérification.
- `sync_logs` : une ligne par source et par passage, avec le statut (`started`, `completed`, `failed`, `skipped`), les compteurs (traités, créés, mis à jour, erreurs) et l'erreur éventuelle.
- `pnpm ingestion:status` affiche la fraîcheur de toutes les sources.

---

## 🧩 Modules principaux (`services/ingestion/src/workers/`)

| Fichier | Rôle |
| :--- | :--- |
| `sync.ts` | Synchronisation de chaque source, rattachements, orchestration `smartSync` et `syncIntraday` |
| `stats-calculator.ts` | Présence, loyauté, participation, alliances, thématiques |
| `mandats.ts`, `backfill-mandats.ts`, `senat-histo.ts`, `mandats-an-sortants.ts` | Mandats parlementaires multi-législatures, anciens sénateurs, clôture des mandats des députés sortants et de leurs mandats d'organe |
| `changement-chambre.ts` | Un député élu sénateur garde sa fiche, qui passe au Sénat ; son identifiant de l'Assemblée est conservé dans `parlementaires_identifiants` |
| `interventions-syceron.ts`, `interventions-commission.ts`, `avis-commission.ts` | Débats de séance et de commission de l'Assemblée, avis de commission |
| `link-debats-scrutins.ts`, `link-debats-scrutins-senat.ts`, `link-interventions-dossiers.ts`, `link-interventions-ordre-du-jour.ts`, `segmenter-debats-senat.ts` | Rattachement des débats aux scrutins et aux dossiers |
| `liens-amendements-discordants.ts` | Correction des liens scrutin ↔ amendement dont l'article contredit le libellé du scrutin |
| `parents-amendements.ts`, `metadonnees-amendements-senat.ts`, `article-vise-senat.ts` | Sous-amendements, discussions et article visé des amendements |
| `sujet-generator.ts`, `sujet-links-generator.ts` | Sujets et leurs liens sortants |
| `ia-enrichment.ts`, `parlementaire-enrichment.ts` | Enrichissement IA |
| `declarations-sync.ts` | Déclarations HATVP |
| `senat-fonctions.ts`, `senat-dossier-commissions.ts` | Fonctions datées des sénateurs (bureau, commissions, groupes) et saisines des commissions du Sénat |
| `gouvernements.ts` | Gouvernements, postes ministériels, présidences de la République et de l'Assemblée (archive AMO30), rattachement à la fiche |
| `textes-articles.ts` | Texte des articles des textes de l'Assemblée |
| `index-legi.ts` | Index de la base LEGI de la DILA (textes et articles de loi, pour les liens vers Légifrance) |
| `renvois-legi.ts` | Renvois au droit en vigueur dans le dispositif des amendements, rattachés à l'index LEGI (grammaire de `@tricoteuses/tisseuse`) |
| `evenements.ts` | Liste curée des événements institutionnels |

La commande `schedule` (fichier `src/scheduler.ts`) lance un planificateur `node-cron` dans le processus. Elle n'est **pas** utilisée en production, où la planification est confiée à Railway.
