@tricoteuses/senat 2.11.5 → 2.13.0

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.
@@ -68,7 +68,6 @@ const findAllAmendementsQuery = dbSenat
68
68
  .as("nature"),
69
69
  "ameli.amd.id as id",
70
70
  "ameli.amd.amdperid as parent_id",
71
- "ameli.amd.amdrendusim as rendu_similaire_id",
72
71
  "ameli.amd.ideid as identique_id",
73
72
  "ameli.amd.discomid as discussion_commune_id",
74
73
  "ameli.amd.num as numero",
@@ -86,7 +85,6 @@ const findAllAmendementsQuery = dbSenat
86
85
  "ameli.sub.dupl as subdivision_dupliquee",
87
86
  "ameli.typsub.lib as subdivision_type",
88
87
  "ameli.amd.alinea as alinea",
89
- "ameli.amd.commentprobleme as commentaire_probleme",
90
88
  "ameli.amd.obs as observations",
91
89
  "ameli.amd.mot as observations_additionnelles",
92
90
  toDateString(ref("ameli.amd.datdep")).as("date_depot"),
@@ -0,0 +1,9 @@
1
+ import { CompteRendu, Sommaire } from "../types/compte_rendu";
2
+ import { TimeSlot } from "../types/agenda";
3
+ export declare function parseCompteRenduSlotFromFile(xmlFilePath: string, wantedSlot: TimeSlot, firstSlotOfDay?: TimeSlot): Promise<CompteRendu | null>;
4
+ export declare function sessionStartYearFromDate(d: Date): number;
5
+ export declare function parseYYYYMMDD(yyyymmdd: string): Date | null;
6
+ export declare function deriveTitreObjetFromSommaire(sommaire: Sommaire | undefined, slot: TimeSlot): {
7
+ titre: string;
8
+ objet: string;
9
+ };
@@ -0,0 +1,325 @@
1
+ import fs from "fs";
2
+ import * as cheerio from "cheerio";
3
+ import path from "path";
4
+ import { computeIntervalsBySlot } from "../utils/cr_spliting";
5
+ import { norm } from "./util";
6
+ const asArray = (x) => x == null ? [] : Array.isArray(x) ? x : [x];
7
+ const toInt = (s) => Number.isFinite(Number(s)) ? Number(s) : Number.POSITIVE_INFINITY;
8
+ export async function parseCompteRenduSlotFromFile(xmlFilePath, wantedSlot, firstSlotOfDay) {
9
+ try {
10
+ const raw = fs.readFileSync(xmlFilePath, "utf8");
11
+ const $ = cheerio.load(raw, { xml: false });
12
+ const metadonnees = extractMetadonnees($, xmlFilePath);
13
+ const order = $("body *").toArray();
14
+ const idx = new Map(order.map((el, i) => [el, i]));
15
+ const intervalsAll = computeIntervalsBySlot($, idx, firstSlotOfDay);
16
+ const intervals = intervalsAll.filter(iv => iv.slot === wantedSlot);
17
+ if (intervals.length === 0) {
18
+ console.warn(`[CRI] no intervals for ${path.basename(xmlFilePath)} [${wantedSlot}]`);
19
+ return null;
20
+ }
21
+ metadonnees.sommaire = extractSommaireForIntervals($, idx, intervals);
22
+ const points = [];
23
+ let ordre = 0;
24
+ const addPoint = (p) => points.push({ ...p, ordre_absolu_seance: String(++ordre) });
25
+ // Titles
26
+ $("cri\\:titreS1 p.titre_S1").each((_, el) => {
27
+ if (!elementInAnyInterval(el, idx, intervals))
28
+ return;
29
+ const t = normalizeTitle(norm($(el).text() || ""));
30
+ if (t)
31
+ addPoint({ code_grammaire: "TITRE_TEXTE_DISCUSSION", texte: { _: t }, code_style: "Titre" });
32
+ });
33
+ // Interventions
34
+ $("div.intervenant").each((_, block) => {
35
+ if (!elementInAnyInterval(block, idx, intervals))
36
+ return;
37
+ const $block = $(block);
38
+ $block.find([
39
+ "p[class^='titre_S']",
40
+ "p.mention_titre",
41
+ "p.intitule_titre",
42
+ "p.mention_chapitre",
43
+ "p.intitule_chapitre",
44
+ "p.mention_article",
45
+ "p.intitule_article",
46
+ "p.mention_section",
47
+ "p.intitule_section",
48
+ ].join(",")).remove();
49
+ const firstP = $block.find("p").first();
50
+ const speakerLabelRaw = firstP.find(".orateur_nom").text() || firstP.find("a.lien_senfic").text() || "";
51
+ const speakerLabel = dedupeSpeaker(speakerLabelRaw);
52
+ const { mat, nom: nomCRI, qua: quaCRI } = readIntervenantMeta($block);
53
+ const qualFromSpans = extractAndRemoveLeadingQualite($, $block);
54
+ const qualite = norm(decodeHtmlEntities(quaCRI || "")) || qualFromSpans;
55
+ const canonicalName = dedupeSpeaker(nomCRI || speakerLabel);
56
+ const role = roleForSpeaker(speakerLabel) || roleForSpeaker(qualite) || roleForSpeaker(quaCRI || "");
57
+ const speechHtml = sanitizeInterventionHtml($, $block);
58
+ if (!norm(cheerio.load(speechHtml).text() || ""))
59
+ return;
60
+ addPoint({
61
+ code_grammaire: "PAROLE_GENERIQUE",
62
+ roledebat: role,
63
+ orateurs: { orateur: { nom: canonicalName, id: mat || "", qualite } },
64
+ texte: { _: speechHtml },
65
+ });
66
+ });
67
+ const contenu = {
68
+ quantiemes: { journee: metadonnees.dateSeance, session: metadonnees.session },
69
+ point: points,
70
+ };
71
+ return {
72
+ uid: "CRSSN" + xmlFilePath.replace(/^.*?(\d{8}).*$/i, "$1") + `-${wantedSlot}`,
73
+ seanceRef: "RUSN" + xmlFilePath.replace(/^.*?(\d{8}).*$/i, "$1") + "IDS-" + wantedSlot,
74
+ sessionRef: metadonnees.session,
75
+ metadonnees,
76
+ contenu,
77
+ };
78
+ }
79
+ catch (e) {
80
+ console.error(`[CRI] parseSlot error file=${xmlFilePath} slot=${wantedSlot}:`, e);
81
+ return null;
82
+ }
83
+ }
84
+ export function sessionStartYearFromDate(d) {
85
+ // Session (1th oct N → 30 sept N+1)
86
+ const m = d.getMonth();
87
+ const y = d.getFullYear();
88
+ return m >= 9 ? y : y - 1;
89
+ }
90
+ export function parseYYYYMMDD(yyyymmdd) {
91
+ if (!/^\d{8}$/.test(yyyymmdd))
92
+ return null;
93
+ const y = Number(yyyymmdd.slice(0, 4));
94
+ const m = Number(yyyymmdd.slice(4, 6)) - 1;
95
+ const d = Number(yyyymmdd.slice(6, 8));
96
+ const dt = new Date(y, m, d);
97
+ return Number.isFinite(dt.getTime()) ? dt : null;
98
+ }
99
+ export function deriveTitreObjetFromSommaire(sommaire, slot) {
100
+ const items = extractLevel1Items(sommaire);
101
+ const meaningful = items.filter(it => !isBoilerplate(it.label));
102
+ if (meaningful.length === 0) {
103
+ return {
104
+ titre: `Séance publique ${slotLabel(slot)}`,
105
+ objet: "",
106
+ };
107
+ }
108
+ const titre = meaningful[0].label;
109
+ const objet = meaningful.slice(0, 3).map(it => it.label).join(" ; ");
110
+ return { titre, objet };
111
+ }
112
+ function slotLabel(slot) {
113
+ switch (slot) {
114
+ case "MATIN": return "du matin";
115
+ case "APRES-MIDI": return "de l’après-midi";
116
+ case "SOIR": return "du soir";
117
+ default: return "";
118
+ }
119
+ }
120
+ const BOILERPLATE_PATTERNS = [
121
+ /proc(?:è|e)s-?verbal/i,
122
+ /hommages?/i,
123
+ /désignation des vice-?président/i,
124
+ /candidatures? aux?/i,
125
+ /ordre du jour/i,
126
+ /rappels? au règlement/i,
127
+ /communications?/i,
128
+ /dépôts?/i,
129
+ /proclamation/i,
130
+ /présidence de/i,
131
+ /questions? diverses?/i,
132
+ /ouverture de la séance/i,
133
+ /clo(?:t|̂)ure de la séance/i,
134
+ ];
135
+ const isBoilerplate = (label) => !label?.trim() || BOILERPLATE_PATTERNS.some(rx => rx.test(label));
136
+ function extractLevel1Items(sommaire) {
137
+ const level1 = asArray(sommaire?.sommaire1);
138
+ return level1
139
+ .map(el => ({
140
+ numero: toInt(el?.valeur_pts_odj),
141
+ label: String(el?.titreStruct?.intitule ?? "").trim(),
142
+ }))
143
+ .filter(it => !!it.label)
144
+ .sort((a, b) => a.numero - b.numero);
145
+ }
146
+ function stripTrailingPunct(s) { return s.replace(/\s*([:,.;])\s*$/u, "").trim(); }
147
+ function dedupeSpeaker(raw) {
148
+ let s = norm(raw);
149
+ s = stripTrailingPunct(s);
150
+ const dupPatterns = [/^(.+?)\s*[.]\s*\1$/u, /^(.+?)\s*,\s*\1,?$/u, /^(.+?)\s+\1$/u];
151
+ for (const re of dupPatterns) {
152
+ const m = s.match(re);
153
+ if (m) {
154
+ s = m[1];
155
+ break;
156
+ }
157
+ }
158
+ return s.replace(/\.\s*$/, "");
159
+ }
160
+ function decodeHtmlEntities(s) {
161
+ return s.replace(/&#(\d+);/g, (_, d) => String.fromCharCode(parseInt(d, 10)))
162
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
163
+ }
164
+ function fixApostrophes(s) {
165
+ let out = s;
166
+ out = out.replace(/\s*’\s*/g, "’");
167
+ out = out.replace(/\b([dljctmsn])\s*’/gi, (_, m) => m + "’");
168
+ out = out.replace(/’\s+([A-Za-zÀ-ÖØ-öø-ÿ])/g, "’$1");
169
+ out = out.replace(/\s+([,;:.!?])/g, "$1");
170
+ return out;
171
+ }
172
+ function normalizeTitle(text) { return text.replace(/^PR[ÉE]SIDENCE DE\b/i, "Présidence de "); }
173
+ function roleForSpeaker(labelOrQualite) {
174
+ const s = (labelOrQualite || "").toLowerCase();
175
+ if (/^(m\.|mme)?\s*(le|la)\s+pr[ée]sident(e)?\b/.test(s) || /\bpr[ée]sident[e]?\s+de\s+séance\b/.test(s))
176
+ return "président";
177
+ return "";
178
+ }
179
+ function readIntervenantMeta($block) {
180
+ const int = $block.find('cri\\:intervenant').first();
181
+ if (int.length)
182
+ return { mat: int.attr("mat") || undefined, nom: int.attr("nom") || undefined, qua: int.attr("qua") || undefined };
183
+ const html = $block.html() || "";
184
+ const m = html.match(/<!--\s*cri:intervenant\b([^>]+)-->/i);
185
+ if (!m)
186
+ return {};
187
+ const out = {};
188
+ const re = /(\w+)="([^"]*)"/g;
189
+ let a;
190
+ while ((a = re.exec(m[1])))
191
+ out[a[1]] = decodeHtmlEntities(a[2]);
192
+ return { mat: out["mat"], nom: out["nom"], qua: out["qua"] };
193
+ }
194
+ function extractAndRemoveLeadingQualite($, $block) {
195
+ const firstP = $block.find("p").first();
196
+ if (firstP.length === 0)
197
+ return "";
198
+ const parts = [];
199
+ let stop = false;
200
+ firstP.contents().each((_, node) => {
201
+ if (stop)
202
+ return;
203
+ if (node.type === "tag") {
204
+ const $node = $(node);
205
+ if ($node.hasClass("orateur_nom")) {
206
+ $node.remove();
207
+ return;
208
+ }
209
+ if ($node.hasClass("orateur_qualite")) {
210
+ parts.push($node.text() || "");
211
+ $node.remove();
212
+ return;
213
+ }
214
+ const t = norm($node.text() || "");
215
+ if (t)
216
+ stop = true;
217
+ else
218
+ $node.remove();
219
+ }
220
+ else if (node.type === "text") {
221
+ const t = norm(node.data || "");
222
+ if (!t || /^[:.,;–—-]+$/.test(t)) {
223
+ node.data = "";
224
+ return;
225
+ }
226
+ stop = true;
227
+ }
228
+ });
229
+ return fixApostrophes(norm(parts.join(" ")));
230
+ }
231
+ function sanitizeInterventionHtml($, $block) {
232
+ const $clone = $block.clone();
233
+ $clone.find('a[name]').remove();
234
+ $clone.find('div[align="right"]').remove();
235
+ $clone.find('a.link').remove();
236
+ $clone.find('img').remove();
237
+ $clone.find('a#ameli_amendement_cri_phrase, a#ameli_amendement_cra_contenu, a#ameli_amendement_cra_objet').remove();
238
+ $clone.find(".orateur_nom, .orateur_qualite").remove();
239
+ let html = $clone.html() || "";
240
+ html = html.replace(/<!--[\s\S]*?-->/g, "");
241
+ return html.trim();
242
+ }
243
+ function extractSommaireForIntervals($, idx, intervals) {
244
+ const inIv = (el) => elementInAnyInterval(el, idx, intervals);
245
+ const root = $("body");
246
+ const sommaire = { presidentSeance: { _: "" }, sommaire1: [] };
247
+ // (1) Présidence (tm2) — première ligne dans l’intervalle
248
+ const pres = root.find("p.tm2").filter((_, el) => inIv(el)).first();
249
+ if (pres.length)
250
+ sommaire.presidentSeance = { _: norm(pres.text()) };
251
+ // (2) Paras tm5 présents dans l’intervalle
252
+ const paras = [];
253
+ root.find("p.tm5").each((_, el) => {
254
+ if (!inIv(el))
255
+ return;
256
+ const t = norm($(el).text());
257
+ if (t)
258
+ paras.push({ _: t });
259
+ });
260
+ if (paras.length)
261
+ sommaire.para = paras.length === 1 ? paras[0] : paras;
262
+ // (3) Items de 1er niveau (tm3) présents dans l’intervalle
263
+ const items = [];
264
+ root.find("p.tm3").each((_, el) => {
265
+ if (!inIv(el))
266
+ return;
267
+ const $p = $(el);
268
+ const full = norm($p.text() || "");
269
+ if (!full)
270
+ return;
271
+ const numMatch = full.match(/^(\d+)\s*[.\-–—]\s*/);
272
+ const valeur = numMatch ? numMatch[1] : undefined;
273
+ // prefere intitule in ancre <a> if present
274
+ const a = $p.find("a").first();
275
+ const intituleRaw = a.length ? a.text() : full.replace(/^(\d+)\s*[.\-–—]\s*/, "");
276
+ const intitule = norm(intituleRaw);
277
+ // id_syceron from href="#Niv1_SOMx"
278
+ const href = (a.attr("href") || "").trim();
279
+ const idSyceron = href.startsWith("#") ? href.slice(1) : href;
280
+ const titreStruct = { id_syceron: idSyceron || "", intitule };
281
+ items.push({ valeur_pts_odj: valeur, titreStruct });
282
+ });
283
+ if (items.length)
284
+ sommaire.sommaire1 = items;
285
+ return sommaire;
286
+ }
287
+ function extractMetadonnees($, filePath) {
288
+ let dateText = norm($("h1, h2, .page-title").first().text() || "");
289
+ if (!dateText)
290
+ dateText = norm($("p").first().text() || "");
291
+ const dateMatch = dateText.match(/\b(\d{1,2}\s+\w+\s+\d{4})\b/i);
292
+ const allText = norm($("body").text() || "");
293
+ const sessionMatch = allText.match(/\bsession\s+(\d{4}-\d{4})\b/i);
294
+ let dateSeance = dateMatch?.[1] || "";
295
+ if (!dateSeance) {
296
+ const m = filePath.match(/d(\d{4})(\d{2})(\d{2})\.xml$/i);
297
+ if (m)
298
+ dateSeance = `${m[1]}-${m[2]}-${m[3]}`;
299
+ }
300
+ return {
301
+ dateSeance,
302
+ dateSeanceJour: dateSeance,
303
+ numSeanceJour: "",
304
+ numSeance: "",
305
+ typeAssemblee: "SN",
306
+ legislature: "",
307
+ session: sessionMatch?.[1] || "",
308
+ nomFichierJo: "",
309
+ validite: "",
310
+ etat: "",
311
+ diffusion: "",
312
+ version: "1.0",
313
+ environnement: "",
314
+ heureGeneration: new Date()
315
+ };
316
+ }
317
+ function elementInAnyInterval(el, idx, intervals) {
318
+ const p = idx.get(el);
319
+ if (p == null)
320
+ return false;
321
+ for (const iv of intervals)
322
+ if (p >= iv.start && p < iv.end)
323
+ return true;
324
+ return false;
325
+ }
@@ -19,10 +19,6 @@ export interface Amd {
19
19
  * Identifiant de l'amendement pere pour les sous-amendements
20
20
  */
21
21
  amdperid: number | null;
22
- /**
23
- * Identifiant de l'amendement auquel celui-ci a ?t? rendu similaire
24
- */
25
- amdrendusim: number | null;
26
22
  /**
27
23
  * Indication de la mention -Et plusieurs de ses collegues-
28
24
  */
@@ -39,10 +35,6 @@ export interface Amd {
39
35
  * Indication de la mendion -Et plusieurs de ses collegues- (uniquement pour les amendements de commission)
40
36
  */
41
37
  colleg: Generated<string>;
42
- /**
43
- * Commentaire sur les probl?mes rencontr?s lors du traitement de l'amendement
44
- */
45
- commentprobleme: string | null;
46
38
  /**
47
39
  * Date de depot de l'amendement
48
40
  */
@@ -59,10 +51,6 @@ export interface Amd {
59
51
  * Identifiant de l'etat de l'amendement
60
52
  */
61
53
  etaid: number;
62
- /**
63
- * Identifiant de l'?tat de traitement d'un amendement (lien fait ? partir d'un enum dans le back)
64
- */
65
- etatraitid: Generated<number>;
66
54
  /**
67
55
  * Identifiant
68
56
  */
@@ -83,7 +71,6 @@ export interface Amd {
83
71
  * Identit? de l'entit? qui a saisi l'irrecevabilit?
84
72
  */
85
73
  irrsaisiepar: number | null;
86
- islu: Generated<string | null>;
87
74
  /**
88
75
  * Libelle complementaire (type d'appartenance au groupe)
89
76
  */
@@ -152,14 +139,6 @@ export interface Amd {
152
139
  * Identification des amendements portant sur article additionnel (si different de 0)
153
140
  */
154
141
  subpos: Generated<Int8 | null>;
155
- /**
156
- * Date de la derni?re modification de l'amendement
157
- */
158
- traitementdate: Timestamp | null;
159
- /**
160
- * Identifiant de la derni?re entit? ? avoir modifi? l'amendement
161
- */
162
- traitemententid: number | null;
163
142
  /**
164
143
  * Identifiant du texte amende
165
144
  */