@tricoteuses/senat 3.1.19 → 3.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE.md +32 -32
  2. package/README.md +326 -326
  3. package/lib/src/other_types/collaborateurs.d.ts +5 -0
  4. package/lib/src/other_types/manifest.d.ts +11 -0
  5. package/lib/src/parsers/collaborateurs.d.ts +44 -0
  6. package/lib/src/parsers/collaborateurs.js +157 -0
  7. package/lib/src/scripts/retrieve_agenda.js +38 -4
  8. package/lib/src/scripts/retrieve_collaborateurs.js +166 -0
  9. package/lib/src/scripts/retrieve_videos.js +5 -1
  10. package/lib/src/scripts/shared/incremental_import_sql.js +859 -859
  11. package/lib/src/scripts/shared/schema_version.js +84 -84
  12. package/lib/src/scripts/shared/staging_metadata_sql.js +214 -214
  13. package/lib/src/scripts/validate_prefixed_tables.js +12 -12
  14. package/lib/src/server/ameli.js +11 -11
  15. package/lib/src/server/conversion_textes.js +106 -106
  16. package/lib/src/server/debats.js +10 -10
  17. package/lib/src/server/documents.js +22 -22
  18. package/lib/src/server/dosleg.js +33 -33
  19. package/lib/src/server/questions.js +10 -10
  20. package/lib/src/server/scrutins.js +3 -3
  21. package/lib/src/server/sens.js +19 -19
  22. package/lib/src/utils/manifest.d.ts +6 -0
  23. package/lib/src/utils/manifest.js +37 -0
  24. package/lib/src/utils/reunion_deletion.d.ts +2 -0
  25. package/lib/src/utils/reunion_deletion.js +27 -0
  26. package/lib/src/videos/match.js +4 -4
  27. package/lib/tests/collaborateurs.test.js +115 -0
  28. package/package.json +117 -117
  29. package/lib/src/config.d.ts +0 -21
  30. package/lib/src/config.js +0 -27
  31. package/lib/src/conversion_textes.d.ts +0 -11
  32. package/lib/src/conversion_textes.js +0 -316
  33. package/lib/src/databases.d.ts +0 -2
  34. package/lib/src/databases.js +0 -26
  35. package/lib/src/datasets.d.ts +0 -34
  36. package/lib/src/datasets.js +0 -233
  37. package/lib/src/git.d.ts +0 -26
  38. package/lib/src/git.js +0 -217
  39. package/lib/src/loaders.d.ts +0 -52
  40. package/lib/src/loaders.js +0 -254
  41. package/lib/src/model/agenda.d.ts +0 -6
  42. package/lib/src/model/agenda.js +0 -148
  43. package/lib/src/model/ameli.d.ts +0 -55
  44. package/lib/src/model/ameli.js +0 -148
  45. package/lib/src/model/commission.d.ts +0 -18
  46. package/lib/src/model/commission.js +0 -269
  47. package/lib/src/model/debats.d.ts +0 -67
  48. package/lib/src/model/debats.js +0 -95
  49. package/lib/src/model/documents.d.ts +0 -12
  50. package/lib/src/model/documents.js +0 -141
  51. package/lib/src/model/dosleg.d.ts +0 -7
  52. package/lib/src/model/dosleg.js +0 -326
  53. package/lib/src/model/index.d.ts +0 -7
  54. package/lib/src/model/index.js +0 -7
  55. package/lib/src/model/questions.d.ts +0 -45
  56. package/lib/src/model/questions.js +0 -89
  57. package/lib/src/model/scrutins.d.ts +0 -13
  58. package/lib/src/model/scrutins.js +0 -114
  59. package/lib/src/model/seance.d.ts +0 -3
  60. package/lib/src/model/seance.js +0 -267
  61. package/lib/src/model/sens.d.ts +0 -182
  62. package/lib/src/model/sens.js +0 -485
  63. package/lib/src/model/util.d.ts +0 -9
  64. package/lib/src/model/util.js +0 -38
  65. package/lib/src/raw_types/ameli.d.ts +0 -914
  66. package/lib/src/raw_types/ameli.js +0 -5
  67. package/lib/src/raw_types/debats.d.ts +0 -207
  68. package/lib/src/raw_types/debats.js +0 -5
  69. package/lib/src/raw_types/dosleg.d.ts +0 -1619
  70. package/lib/src/raw_types/dosleg.js +0 -5
  71. package/lib/src/raw_types/questions.d.ts +0 -423
  72. package/lib/src/raw_types/questions.js +0 -5
  73. package/lib/src/raw_types/senat.d.ts +0 -11372
  74. package/lib/src/raw_types/senat.js +0 -5
  75. package/lib/src/raw_types/sens.d.ts +0 -8248
  76. package/lib/src/raw_types/sens.js +0 -5
  77. package/lib/src/raw_types_schemats/ameli.d.ts +0 -539
  78. package/lib/src/raw_types_schemats/ameli.js +0 -2
  79. package/lib/src/raw_types_schemats/debats.d.ts +0 -127
  80. package/lib/src/raw_types_schemats/debats.js +0 -2
  81. package/lib/src/raw_types_schemats/dosleg.d.ts +0 -977
  82. package/lib/src/raw_types_schemats/dosleg.js +0 -2
  83. package/lib/src/raw_types_schemats/questions.d.ts +0 -237
  84. package/lib/src/raw_types_schemats/questions.js +0 -2
  85. package/lib/src/raw_types_schemats/sens.d.ts +0 -6915
  86. package/lib/src/raw_types_schemats/sens.js +0 -2
  87. package/lib/src/scripts/test_iter_load.js +0 -12
  88. package/lib/src/types/agenda.d.ts +0 -45
  89. package/lib/src/types/ameli.d.ts +0 -5
  90. package/lib/src/types/compte_rendu.d.ts +0 -83
  91. package/lib/src/types/debats.d.ts +0 -2
  92. package/lib/src/types/debats.js +0 -1
  93. package/lib/src/types/dosleg.d.ts +0 -70
  94. package/lib/src/types/dosleg.js +0 -1
  95. package/lib/src/types/questions.d.ts +0 -2
  96. package/lib/src/types/questions.js +0 -1
  97. package/lib/src/types/sens.d.ts +0 -10
  98. package/lib/src/types/sens.js +0 -1
  99. package/lib/src/types/sessions.d.ts +0 -6
  100. package/lib/src/types/sessions.js +0 -19
  101. package/lib/src/types/texte.d.ts +0 -72
  102. package/lib/src/types/texte.js +0 -15
  103. package/lib/src/validators/config.d.ts +0 -9
  104. package/lib/src/validators/config.js +0 -10
  105. /package/lib/src/{scripts/test_iter_load.d.ts → other_types/collaborateurs.js} +0 -0
  106. /package/lib/src/{types/agenda.js → other_types/manifest.js} +0 -0
  107. /package/lib/src/{types/ameli.js → scripts/retrieve_collaborateurs.d.ts} +0 -0
  108. /package/lib/{src/types/compte_rendu.js → tests/collaborateurs.test.d.ts} +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Fonctions pures de parsing du PDF DRH des collaborateurs de sénateurs
3
+ * (https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf).
4
+ *
5
+ * Le PDF est un tableau à deux colonnes par page :
6
+ * - "Employeur" (le sénateur) à gauche, ex. "Mme AESCHLIMANN Marie-Do" ;
7
+ * - "Nom Collaborateur" à droite, ex. "M. CHAREF Dahmane".
8
+ * Une ligne sans employeur prolonge le sénateur précédent.
9
+ *
10
+ * Pièges traités ici : aucun matricule dans le PDF, prénoms de sénateurs tronqués
11
+ * ("Marie-Do"), noms composés ("DI FOLCO", "MAZENOT CHAPPUY"), accents.
12
+ */
13
+ /** Élément de texte positionné extrait du PDF (x croissant vers la droite, y croissant vers le haut). */
14
+ export type PdfTextItem = {
15
+ str: string;
16
+ x: number;
17
+ y: number;
18
+ page: number;
19
+ };
20
+ export type PersonneNom = {
21
+ civilite: string;
22
+ nom: string;
23
+ prenom: string;
24
+ };
25
+ export type LigneCollaborateur = {
26
+ senateur: PersonneNom;
27
+ collaborateur: PersonneNom;
28
+ };
29
+ /** Normalise un texte pour la comparaison : majuscules, sans accents, espaces compactés. */
30
+ export declare function normaliserPourComparaison(valeur: string): string;
31
+ /**
32
+ * Sépare une cellule "Civilité NOM(S) Prénom" en {civilite, nom, prenom}.
33
+ * Les jetons en majuscules de tête forment le nom (gère les noms composés), le reste le prénom.
34
+ * Renvoie null si la cellule n'est pas un nom de personne exploitable.
35
+ */
36
+ export declare function parseCellulePersonne(cellule: string): PersonneNom | null;
37
+ /** Extrait la date d'édition imprimée en pied de page ("Edition du JJ/MM/AAAA"). */
38
+ export declare function extraireDateEdition(strings: string[]): Date | null;
39
+ /**
40
+ * Reconstruit la liste {sénateur, collaborateur} à partir des éléments de texte positionnés.
41
+ * Parcourt les pages puis les lignes de haut en bas ; mémorise le sénateur courant et lui
42
+ * rattache les collaborateurs des lignes suivantes tant qu'aucun nouvel employeur n'apparaît.
43
+ */
44
+ export declare function construireLignesCollaborateurs(items: PdfTextItem[]): LigneCollaborateur[];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Fonctions pures de parsing du PDF DRH des collaborateurs de sénateurs
3
+ * (https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf).
4
+ *
5
+ * Le PDF est un tableau à deux colonnes par page :
6
+ * - "Employeur" (le sénateur) à gauche, ex. "Mme AESCHLIMANN Marie-Do" ;
7
+ * - "Nom Collaborateur" à droite, ex. "M. CHAREF Dahmane".
8
+ * Une ligne sans employeur prolonge le sénateur précédent.
9
+ *
10
+ * Pièges traités ici : aucun matricule dans le PDF, prénoms de sénateurs tronqués
11
+ * ("Marie-Do"), noms composés ("DI FOLCO", "MAZENOT CHAPPUY"), accents.
12
+ */
13
+ // Bandes horizontales des deux colonnes (mesurées sur le PDF : employeur ≈ 147, collaborateur ≈ 285).
14
+ const COLONNE_EMPLOYEUR = { min: 130, max: 225 };
15
+ const COLONNE_COLLABORATEUR = { min: 255, max: 360 };
16
+ const CIVILITES = ["Mme", "M.", "Mlle", "M"];
17
+ // Particules nobiliaires/patronymiques en minuscules, qui font partie du nom quand elles le précèdent
18
+ // (ex. "de CIDRAC", "de LA GONTRIE", "de LEGGE"). Le référentiel Sénat les conserve dans Nom_usuel.
19
+ const PARTICULES = new Set([
20
+ "de",
21
+ "du",
22
+ "des",
23
+ "d'",
24
+ "le",
25
+ "la",
26
+ "les",
27
+ "von",
28
+ "van",
29
+ "der",
30
+ "den",
31
+ "da",
32
+ "di",
33
+ "del",
34
+ "dos",
35
+ ]);
36
+ // Lignes d'en-tête / pied de page à ignorer.
37
+ const LIGNES_IGNOREES = [
38
+ /liste des collaborateurs/i,
39
+ /^employeur$/i,
40
+ /^nom collaborateur$/i,
41
+ /^a\.?g\.?a\.?s\.?/i,
42
+ /edition du/i,
43
+ /congé non rémunéré/i,
44
+ /^-\s*\d+\s*-$/,
45
+ ];
46
+ /** Normalise un texte pour la comparaison : majuscules, sans accents, espaces compactés. */
47
+ export function normaliserPourComparaison(valeur) {
48
+ return valeur
49
+ .normalize("NFD")
50
+ .replace(/\p{Diacritic}/gu, "")
51
+ .toUpperCase()
52
+ .replace(/['']/g, "'")
53
+ .replace(/\s+/g, " ")
54
+ .trim();
55
+ }
56
+ function estIgnoree(str) {
57
+ const t = str.trim();
58
+ if (!t)
59
+ return true;
60
+ return LIGNES_IGNOREES.some((re) => re.test(t));
61
+ }
62
+ /**
63
+ * Sépare une cellule "Civilité NOM(S) Prénom" en {civilite, nom, prenom}.
64
+ * Les jetons en majuscules de tête forment le nom (gère les noms composés), le reste le prénom.
65
+ * Renvoie null si la cellule n'est pas un nom de personne exploitable.
66
+ */
67
+ export function parseCellulePersonne(cellule) {
68
+ const brut = cellule
69
+ .replace(/\(\*\)/g, "")
70
+ .replace(/\s+/g, " ")
71
+ .trim();
72
+ if (!brut)
73
+ return null;
74
+ let civilite = "";
75
+ let reste = brut;
76
+ for (const civ of CIVILITES) {
77
+ if (brut === civ)
78
+ return null;
79
+ if (brut.startsWith(`${civ} `)) {
80
+ civilite = civ;
81
+ reste = brut.slice(civ.length + 1).trim();
82
+ break;
83
+ }
84
+ }
85
+ if (!reste)
86
+ return null;
87
+ const jetons = reste.split(" ");
88
+ const estMajuscule = (jeton) => {
89
+ const lettres = jeton.replace(/[^A-Za-zÀ-ÿ]/g, "");
90
+ return lettres.length > 0 && lettres === lettres.toUpperCase();
91
+ };
92
+ const jetonsNom = [];
93
+ let i = 0;
94
+ // Particules de tête en minuscules (ex. "de", "de la") faisant partie du nom.
95
+ while (i < jetons.length && PARTICULES.has(jetons[i].toLowerCase())) {
96
+ jetonsNom.push(jetons[i]);
97
+ i++;
98
+ }
99
+ // Puis les jetons du nom en majuscules.
100
+ while (i < jetons.length && estMajuscule(jetons[i])) {
101
+ jetonsNom.push(jetons[i]);
102
+ i++;
103
+ }
104
+ // Une particule seule sans nom en majuscules n'est pas un nom valide : on rétablit.
105
+ if (jetonsNom.length > 0 && jetonsNom.every((j) => PARTICULES.has(j.toLowerCase()))) {
106
+ jetonsNom.length = 0;
107
+ i = 0;
108
+ }
109
+ // Si aucun jeton majuscule (cas inattendu), on prend le premier comme nom.
110
+ if (jetonsNom.length === 0 && jetons.length > 0) {
111
+ jetonsNom.push(jetons[0]);
112
+ i = 1;
113
+ }
114
+ const nom = jetonsNom.join(" ");
115
+ const prenom = jetons.slice(i).join(" ");
116
+ if (!nom)
117
+ return null;
118
+ return { civilite, nom, prenom };
119
+ }
120
+ /** Extrait la date d'édition imprimée en pied de page ("Edition du JJ/MM/AAAA"). */
121
+ export function extraireDateEdition(strings) {
122
+ for (const s of strings) {
123
+ const m = s.match(/Edition du\s+(\d{2})\/(\d{2})\/(\d{4})/i);
124
+ if (m) {
125
+ const [, jj, mm, aaaa] = m;
126
+ return new Date(Date.UTC(Number(aaaa), Number(mm) - 1, Number(jj)));
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * Reconstruit la liste {sénateur, collaborateur} à partir des éléments de texte positionnés.
133
+ * Parcourt les pages puis les lignes de haut en bas ; mémorise le sénateur courant et lui
134
+ * rattache les collaborateurs des lignes suivantes tant qu'aucun nouvel employeur n'apparaît.
135
+ */
136
+ export function construireLignesCollaborateurs(items) {
137
+ const utiles = items.filter((it) => !estIgnoree(it.str));
138
+ // Tri lecture : page croissante, puis y décroissant (haut → bas), puis x croissant (gauche → droite).
139
+ const tries = [...utiles].sort((a, b) => a.page - b.page || b.y - a.y || a.x - b.x);
140
+ const lignes = [];
141
+ let senateurCourant = null;
142
+ for (const it of tries) {
143
+ const dansEmployeur = it.x >= COLONNE_EMPLOYEUR.min && it.x <= COLONNE_EMPLOYEUR.max;
144
+ const dansCollaborateur = it.x >= COLONNE_COLLABORATEUR.min && it.x <= COLONNE_COLLABORATEUR.max;
145
+ if (dansEmployeur) {
146
+ const personne = parseCellulePersonne(it.str);
147
+ if (personne)
148
+ senateurCourant = personne;
149
+ }
150
+ else if (dansCollaborateur && senateurCourant) {
151
+ const collaborateur = parseCellulePersonne(it.str);
152
+ if (collaborateur)
153
+ lignes.push({ senateur: senateurCourant, collaborateur });
154
+ }
155
+ }
156
+ return lignes;
157
+ }
@@ -11,6 +11,8 @@ import { assertExistingDirectory, commonOptions } from "./shared/cli_helpers.js"
11
11
  import { fetchWithRetry } from "./shared/util.js";
12
12
  import { buildReunionsByBucket } from "../utils/reunion_parsing.js";
13
13
  import { buildSenatDossierIndex, buildDoslegReunionIndex } from "../utils/reunion_odj_building.js";
14
+ import { loadPreviousManifest, saveManifest, detectChanges } from "../utils/manifest.js";
15
+ import { deleteReunionFiles } from "../utils/reunion_deletion.js";
14
16
  const optionsDefinitions = [
15
17
  ...commonOptions,
16
18
  {
@@ -52,11 +54,9 @@ async function retrieveAgendas(options, sessions) {
52
54
  for (const session of sessions) {
53
55
  const originalAgendaSessionDir = path.join(originalAgendaDir, `${session}`);
54
56
  fs.ensureDirSync(originalAgendaSessionDir);
55
- fs.emptyDirSync(originalAgendaSessionDir);
56
57
  const transformedAgendaSessionDir = path.join(transformedAgendaDir, `${session}`);
57
58
  if (options["parseAgenda"]) {
58
59
  fs.ensureDirSync(transformedAgendaSessionDir);
59
- fs.emptyDirSync(transformedAgendaSessionDir);
60
60
  }
61
61
  const fifteenDaysFromNow = new Date();
62
62
  // Don't download agendas more than 15 days in the future.
@@ -68,7 +68,8 @@ async function retrieveAgendas(options, sessions) {
68
68
  try {
69
69
  await downloadAgenda(agendaName, agendaPath);
70
70
  if (options["parseAgenda"]) {
71
- await parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPath, dossierIndex, doslegReunionIndex);
71
+ const createdUids = await parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPath, dossierIndex, doslegReunionIndex);
72
+ await processManifestAndDeletions(transformedAgendaSessionDir, agendaFileName, session, createdUids);
72
73
  }
73
74
  }
74
75
  catch (error) {
@@ -109,21 +110,54 @@ async function parseAgenda(transformedAgendaSessionDir, agendaFileName, agendaPa
109
110
  console.log(`Parsing Agenda ${agendaPath}…`);
110
111
  const parsedAgendaEvents = await parseAgendaFromFile(agendaPath);
111
112
  if (!parsedAgendaEvents?.length)
112
- return;
113
+ return [];
113
114
  const flatPath = path.join(transformedAgendaSessionDir, `${agendaFileName}.json`);
114
115
  fs.writeJSONSync(flatPath, parsedAgendaEvents, { spaces: 2 });
115
116
  const byBucket = buildReunionsByBucket(parsedAgendaEvents, dossierBySenatUrl, doslegReunionIndex);
117
+ // Collecter tous les UIDs créés
118
+ const createdUids = [];
116
119
  // SP
117
120
  if (byBucket.IDS.length > 0) {
118
121
  writeGroupsAsFiles(transformedAgendaSessionDir, byBucket.IDS);
122
+ createdUids.push(...byBucket.IDS.map((r) => r.uid));
119
123
  }
120
124
  // NON-SP
121
125
  for (const suffix of ["IDC", "IDM", "IDO", "IDI"]) {
122
126
  const groups = byBucket[suffix];
123
127
  if (groups.length > 0) {
124
128
  writeGroupsAsFiles(transformedAgendaSessionDir, groups);
129
+ createdUids.push(...groups.map((r) => r.uid));
130
+ }
131
+ }
132
+ return createdUids;
133
+ }
134
+ async function processManifestAndDeletions(transformedAgendaSessionDir, agendaFileName, session, currentUids) {
135
+ const date = agendaFileName;
136
+ const previousManifest = loadPreviousManifest(transformedAgendaSessionDir, date);
137
+ if (previousManifest) {
138
+ const changes = detectChanges(previousManifest.reunionUids, currentUids);
139
+ if (changes.new.length > 0) {
140
+ console.log(` [${date}] ${changes.new.length} nouvelle(s) réunion(s)`);
125
141
  }
142
+ if (changes.deleted.length > 0) {
143
+ console.log(` [${date}] ${changes.deleted.length} réunion(s) supprimée(s) détectée(s)`);
144
+ const deleted = deleteReunionFiles(transformedAgendaSessionDir, changes.deleted);
145
+ console.log(` [${date}] ${deleted} fichier(s) supprimé(s)`);
146
+ }
147
+ if (changes.kept.length > 0 && !options["silent"]) {
148
+ console.log(` [${date}] ${changes.kept.length} réunion(s) conservée(s)`);
149
+ }
150
+ }
151
+ else if (!options["silent"]) {
152
+ console.log(` [${date}] Premier téléchargement (${currentUids.length} réunions)`);
126
153
  }
154
+ const newManifest = {
155
+ date,
156
+ timestamp: DateTime.now().toISO() ?? "",
157
+ reunionUids: currentUids,
158
+ session,
159
+ };
160
+ saveManifest(transformedAgendaSessionDir, newManifest);
127
161
  }
128
162
  async function main() {
129
163
  const dataDir = assertExistingDirectory(options["dataDir"], "data directory");
@@ -0,0 +1,166 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { getDocumentProxy } from "unpdf";
4
+ import { construireLignesCollaborateurs, extraireDateEdition, normaliserPourComparaison, } from "../parsers/collaborateurs.js";
5
+ import { assertExistingDirectory } from "./shared/cli_helpers.js";
6
+ import { iterFilePaths } from "../server/loaders.js";
7
+ const URL_PDF_DEFAUT = "https://www.senat.fr/pubagas/liste_senateurs_collaborateurs.pdf";
8
+ const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36";
9
+ async function telechargerPdf(url) {
10
+ const cookies = new Map();
11
+ let courant = url;
12
+ for (let i = 0; i < 8; i += 1) {
13
+ const headers = { "User-Agent": USER_AGENT };
14
+ if (cookies.size > 0) {
15
+ headers["Cookie"] = [...cookies].map(([k, v]) => `${k}=${v}`).join("; ");
16
+ }
17
+ const reponse = await fetch(courant, { redirect: "manual", headers });
18
+ for (const brut of reponse.headers.getSetCookie?.() ?? []) {
19
+ const [paire] = brut.split(";");
20
+ const idx = paire.indexOf("=");
21
+ if (idx > 0)
22
+ cookies.set(paire.slice(0, idx).trim(), paire.slice(idx + 1).trim());
23
+ }
24
+ if (reponse.status >= 300 && reponse.status < 400) {
25
+ const location = reponse.headers.get("location");
26
+ if (!location)
27
+ throw new Error(`Redirection sans en-tête Location (HTTP ${reponse.status})`);
28
+ courant = new URL(location, courant).toString();
29
+ continue;
30
+ }
31
+ if (!reponse.ok)
32
+ throw new Error(`Téléchargement du PDF échoué : HTTP ${reponse.status}`);
33
+ const buffer = Buffer.from(await reponse.arrayBuffer());
34
+ if (buffer.subarray(0, 5).toString("latin1") !== "%PDF-") {
35
+ throw new Error("Le contenu téléchargé n'est pas un PDF");
36
+ }
37
+ return buffer;
38
+ }
39
+ throw new Error("Trop de redirections lors du téléchargement du PDF DRH");
40
+ }
41
+ async function extraireItemsPdf(buffer) {
42
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
43
+ const items = [];
44
+ const strings = [];
45
+ for (let p = 1; p <= pdf.numPages; p += 1) {
46
+ const page = await pdf.getPage(p);
47
+ const contenu = await page.getTextContent();
48
+ for (const item of contenu.items) {
49
+ if (typeof item.str !== "string" || !item.str.trim() || !item.transform)
50
+ continue;
51
+ items.push({ str: item.str, x: item.transform[4], y: item.transform[5], page: p });
52
+ strings.push(item.str);
53
+ }
54
+ }
55
+ return { items, strings };
56
+ }
57
+ function chargerSenateurs(dataDir) {
58
+ const senateursDir = path.join(dataDir, "sens", "senateurs");
59
+ const files = new Map();
60
+ const indexParNom = new Map();
61
+ if (!fs.existsSync(senateursDir))
62
+ return { files, indexParNom };
63
+ for (const filePath of iterFilePaths(senateursDir)) {
64
+ const senator = JSON.parse(fs.readFileSync(filePath, "utf-8"));
65
+ files.set(senator.matricule, senator);
66
+ const cle = normaliserPourComparaison(senator.nom_usuel);
67
+ const liste = indexParNom.get(cle);
68
+ if (liste)
69
+ liste.push(senator);
70
+ else
71
+ indexParNom.set(cle, [senator]);
72
+ }
73
+ return { files, indexParNom };
74
+ }
75
+ async function main() {
76
+ const dataDir = process.argv[2];
77
+ assertExistingDirectory(dataDir, "dataDir");
78
+ // 1. Téléchargement + parsing du PDF
79
+ console.log(`Téléchargement du PDF DRH : ${URL_PDF_DEFAUT}`);
80
+ const buffer = await telechargerPdf(URL_PDF_DEFAUT);
81
+ const { items, strings } = await extraireItemsPdf(buffer);
82
+ const lignes = construireLignesCollaborateurs(items);
83
+ const dateEdition = extraireDateEdition(strings);
84
+ console.log(`PDF lu : ${lignes.length} lignes collaborateur, édition ${dateEdition?.toISOString().slice(0, 10) ?? "inconnue"}`);
85
+ if (lignes.length === 0) {
86
+ throw new Error("Aucune ligne collaborateur extraite du PDF : format inattendu, abandon sans modification.");
87
+ }
88
+ // 2. Charger les fichiers sénateurs existants
89
+ const { indexParNom } = chargerSenateurs(dataDir);
90
+ if (indexParNom.size === 0) {
91
+ throw new Error("Aucun fichier sénateur trouvé. Lancez d'abord data:download pour générer les fichiers.");
92
+ }
93
+ // 3. Résoudre les sénateurs du PDF → fichiers
94
+ let nbEnrichis = 0;
95
+ let nbNonResolus = 0;
96
+ // Grouper les collaborateurs par matricule de sénateur
97
+ const collabsParMatricule = new Map();
98
+ const nomsNonResolus = new Set();
99
+ for (const ligne of lignes) {
100
+ const cleNom = normaliserPourComparaison(ligne.senateur.nom);
101
+ const candidats = indexParNom.get(cleNom);
102
+ if (!candidats || candidats.length === 0) {
103
+ nomsNonResolus.add(`${ligne.senateur.civilite} ${ligne.senateur.nom} ${ligne.senateur.prenom}`.trim());
104
+ nbNonResolus += 1;
105
+ continue;
106
+ }
107
+ // Désambiguïsation par préfixe de prénom
108
+ const prenomCible = normaliserPourComparaison(ligne.senateur.prenom);
109
+ const parPrenom = candidats.filter((c) => normaliserPourComparaison(c.prenom_usuel).startsWith(prenomCible));
110
+ let senator = null;
111
+ if (candidats.length === 1) {
112
+ senator = candidats[0];
113
+ }
114
+ else if (parPrenom.length === 1) {
115
+ senator = parPrenom[0];
116
+ }
117
+ else {
118
+ nomsNonResolus.add(`${ligne.senateur.civilite} ${ligne.senateur.nom} ${ligne.senateur.prenom}`.trim());
119
+ nbNonResolus += 1;
120
+ continue;
121
+ }
122
+ let collabs = collabsParMatricule.get(senator.matricule);
123
+ if (!collabs) {
124
+ collabs = [];
125
+ collabsParMatricule.set(senator.matricule, collabs);
126
+ }
127
+ // Dédoublonnage
128
+ const nNom = normaliserPourComparaison(ligne.collaborateur.nom);
129
+ const nPrenom = normaliserPourComparaison(ligne.collaborateur.prenom);
130
+ if (!collabs.some((c) => normaliserPourComparaison(c.nom) === nNom && normaliserPourComparaison(c.prenom) === nPrenom)) {
131
+ collabs.push({
132
+ civilite: ligne.collaborateur.civilite,
133
+ nom: ligne.collaborateur.nom,
134
+ prenom: ligne.collaborateur.prenom,
135
+ });
136
+ }
137
+ }
138
+ // 4. Écrire les collaborateurs dans les fichiers sénateurs
139
+ for (const [matricule, collaborateurs] of collabsParMatricule) {
140
+ const cheminFichier = path.join(dataDir, "sens", "senateurs", `${matricule}.json`);
141
+ if (!fs.existsSync(cheminFichier))
142
+ continue;
143
+ const senator = JSON.parse(fs.readFileSync(cheminFichier, "utf-8"));
144
+ senator.collaborateurs = collaborateurs;
145
+ fs.writeFileSync(cheminFichier, JSON.stringify(senator, null, 2) + "\n");
146
+ nbEnrichis += 1;
147
+ }
148
+ // 5. Nettoyer les collaborateurs des sénateurs absents du PDF
149
+ const matriculesEnrichis = new Set(collabsParMatricule.keys());
150
+ const { files: tousLesFichiers } = chargerSenateurs(dataDir);
151
+ for (const [matricule, senator] of tousLesFichiers) {
152
+ if (!matriculesEnrichis.has(matricule) && senator.collaborateurs) {
153
+ delete senator.collaborateurs;
154
+ const cheminFichier = path.join(dataDir, "sens", "senateurs", `${matricule}.json`);
155
+ fs.writeFileSync(cheminFichier, JSON.stringify(senator, null, 2) + "\n");
156
+ }
157
+ }
158
+ console.log(`${nbEnrichis} sénateur(s) enrichi(s) avec leurs collaborateurs`);
159
+ if (nomsNonResolus.size > 0) {
160
+ console.warn(`${nbNonResolus} sénateur(s) du PDF non rattaché(s) : ${[...nomsNonResolus].slice(0, 10).join(", ")}${nomsNonResolus.size > 10 ? "…" : ""}`);
161
+ }
162
+ }
163
+ main().catch((error) => {
164
+ console.error(error);
165
+ process.exit(1);
166
+ });
@@ -42,7 +42,11 @@ function shouldSkipDownload(agenda, baseDir) {
42
42
  const cutoff = now - options["only-recent"] * 24 * 3600 * 1000;
43
43
  const reunionTs = Date.parse(agenda.date);
44
44
  if (reunionTs >= cutoff)
45
- return false;
45
+ return false; // recent: always try
46
+ // Old meeting with no captation: will never match (minAccept=0.9), skip
47
+ if (!agenda.captationVideo)
48
+ return true;
49
+ // Old meeting with captation: skip only if already downloaded
46
50
  const dataNvsPath = path.join(baseDir, "data.nvs");
47
51
  const finalplayerNvsPath = path.join(baseDir, "finalplayer.nvs");
48
52
  return fs.existsSync(dataNvsPath) && fs.existsSync(finalplayerNvsPath);