@tricoteuses/senat 3.1.21 → 3.1.23

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.
@@ -39,13 +39,6 @@ function getDateSortValue(value) {
39
39
  function compareByDate(left, right) {
40
40
  return getDateSortValue(left.date) - getDateSortValue(right.date);
41
41
  }
42
- function getCommissionCodeActe(phasePrefix, rapportType) {
43
- const normalizedType = (rapportType || "").toLowerCase();
44
- if (normalizedType.includes("avis") || normalizedType.includes("information")) {
45
- return `${phasePrefix}-COM-AVIS`;
46
- }
47
- return `${phasePrefix}-COM-FOND`;
48
- }
49
42
  function getPhasePrefix(lecture, assemblee, codeNatureDossier) {
50
43
  if (assemblee !== "Sénat")
51
44
  return null;
@@ -154,12 +154,11 @@ async function convertDatasetDosLeg(dataDir, options) {
154
154
  if (options["verbose"]) {
155
155
  console.log(`Converting ${dossier["signet"]} file…`);
156
156
  }
157
- let dossierReorganizedDir = path.join(dossiersReorganizedDir, String(UNDEFINED_SESSION));
158
157
  const session = getSessionFromSignet(dossier["signet"]) || UNDEFINED_SESSION;
159
158
  if (options["fromSession"] && session < options["fromSession"]) {
160
159
  continue;
161
160
  }
162
- dossierReorganizedDir = path.join(dossiersReorganizedDir, String(session));
161
+ const dossierReorganizedDir = path.join(dossiersReorganizedDir, String(session));
163
162
  const actesBrutsNormalises = buildActesLegislatifs(dossier);
164
163
  const dossierWithActes = {
165
164
  ...dossier,
@@ -184,9 +183,8 @@ async function convertDatasetScrutins(dataDir, options) {
184
183
  if (options["verbose"]) {
185
184
  console.log(`Converting ${scrutin["numero"]} file…`);
186
185
  }
187
- let scrutinReorganizedDir = path.join(scrutinsReorganizedDir, String(UNDEFINED_SESSION));
188
186
  const session = scrutin["session"] || UNDEFINED_SESSION;
189
- scrutinReorganizedDir = path.join(scrutinsReorganizedDir, String(session));
187
+ const scrutinReorganizedDir = path.join(scrutinsReorganizedDir, String(session));
190
188
  const scrutinFileName = `${scrutin["numero"]}.json`;
191
189
  await fs.outputJSON(path.join(scrutinReorganizedDir, scrutinFileName), scrutin, {
192
190
  spaces: 2,
@@ -339,7 +337,8 @@ async function convertDatasetSens(dataDir, options) {
339
337
  for await (const organisme of findAllOrganismes()) {
340
338
  const sanitizedCode = organisme.code.trim().replace(/\s+/g, "-");
341
339
  if (sanitizedCode !== organisme.code) {
342
- console.warn(`Warning: code "${organisme.code}" contains leading/trailing or multiple consecutive whitespace characters. It has been sanitized to "${sanitizedCode}". Please check the original code for potential issues.`);
340
+ console.warn(`Warning: code "${organisme.code}" contains leading/trailing or multiple consecutive whitespace characters. ` +
341
+ `It has been sanitized to "${sanitizedCode}". Please check the original code for potential issues.`);
343
342
  }
344
343
  if (options["verbose"]) {
345
344
  console.log(`Converting ${sanitizedCode} file…`);
@@ -263,7 +263,7 @@ async function retrieveCommissionCRs(options = {}) {
263
263
  for (const f of htmlFiles) {
264
264
  const htmlPath = path.join(commissionDir, f);
265
265
  let meta;
266
- let raw = "";
266
+ let raw;
267
267
  try {
268
268
  raw = await fs.readFile(htmlPath, "utf8");
269
269
  meta = parseCommissionMetadataFromHtml(raw, f);
@@ -28,18 +28,25 @@ const optionsDefinitions = [
28
28
  const options = commandLineArgs(optionsDefinitions);
29
29
  const CRI_ZIP_URL = "https://data.senat.fr/data/debats/cri.zip";
30
30
  let exitCode = 10; // 0: some data changed, 10: no modification
31
+ function log(options, ...args) {
32
+ if (!options["silent"])
33
+ console.log(...args);
34
+ }
35
+ function warn(options, ...args) {
36
+ if (!options["silent"])
37
+ console.warn(...args);
38
+ }
31
39
  class CompteRenduError extends Error {
32
40
  constructor(message, url) {
33
41
  super(`An error occurred while retrieving ${url}: ${message}`);
34
42
  }
35
43
  }
36
- async function downloadCriZip(zipPath) {
37
- if (!options["silent"])
38
- console.log(`Downloading CRI zip ${CRI_ZIP_URL}…`);
44
+ async function downloadCriZip(zipPath, options) {
45
+ log(options, `Downloading CRI zip ${CRI_ZIP_URL}…`);
39
46
  const response = await fetchWithRetry(CRI_ZIP_URL);
40
47
  if (!response.ok) {
41
48
  if (response.status === 404) {
42
- console.warn(`CRI zip ${CRI_ZIP_URL} not found`);
49
+ warn(options, `CRI zip ${CRI_ZIP_URL} not found`);
43
50
  return;
44
51
  }
45
52
  throw new CompteRenduError(String(response.status), CRI_ZIP_URL);
@@ -48,7 +55,7 @@ async function downloadCriZip(zipPath) {
48
55
  await fs.writeFile(zipPath, buf);
49
56
  if (!options["silent"]) {
50
57
  const mb = (buf.length / (1024 * 1024)).toFixed(1);
51
- console.log(`[CRI] Downloaded ${mb} MB → ${zipPath}`);
58
+ log(options, `[CRI] Downloaded ${mb} MB → ${zipPath}`);
52
59
  }
53
60
  }
54
61
  async function extractAndDistributeXmlBySession(zipPath, originalRoot) {
@@ -97,9 +104,9 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
97
104
  const sessions = getSessionsFromStart((options["fromSession"] ?? UNDEFINED_SESSION));
98
105
  // 1) Download ZIP global + distribut by session
99
106
  const zipPath = path.join(dataDir, "cri.zip");
100
- console.log("[CRI] Downloading global CRI zip…");
101
- await downloadCriZip(zipPath);
102
- console.log("[CRI] Extracting + distributing XMLs by session…");
107
+ log(options, "[CRI] Downloading global CRI zip…");
108
+ await downloadCriZip(zipPath, options);
109
+ log(options, "[CRI] Extracting + distributing XMLs by session…");
103
110
  for (const session of sessions) {
104
111
  const dir = path.join(originalRoot, String(session));
105
112
  if (await fs.pathExists(dir)) {
@@ -110,13 +117,13 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
110
117
  }
111
118
  const n = await extractAndDistributeXmlBySession(zipPath, originalRoot);
112
119
  if (n === 0) {
113
- console.warn("[CRI] No XML extracted. Archive empty or layout changed?");
120
+ warn(options, "[CRI] No XML extracted. Archive empty or layout changed?");
114
121
  }
115
122
  else {
116
- console.log(`[CRI] Distributed ${n} XML file(s) into session folders.`);
123
+ log(options, `[CRI] Distributed ${n} XML file(s) into session folders.`);
117
124
  }
118
125
  if (!options["parseDebats"]) {
119
- console.log("[CRI] parseDebats not requested → done.");
126
+ log(options, "[CRI] parseDebats not requested → done.");
120
127
  return;
121
128
  }
122
129
  for (const session of sessions) {
@@ -147,10 +154,10 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
147
154
  const crPath = path.join(transformedSessionDir, fn);
148
155
  try {
149
156
  const cr = await fs.readJSON(crPath);
150
- await linkCriEventIntoAgenda(dataDir, yyyymmdd, eventId, cr.uid, cr, session);
157
+ await linkCriEventIntoAgenda(dataDir, yyyymmdd, eventId, cr.uid, cr, session, options);
151
158
  }
152
159
  catch (e) {
153
- console.warn(`[CR] [${session}] Could not relink existing CR into a reunion for ${yyyymmdd} event=${eventId}:`, e);
160
+ warn(options, `[CR] [${session}] Could not relink existing CR into a reunion for ${yyyymmdd} event=${eventId}:`, e);
154
161
  }
155
162
  }
156
163
  continue;
@@ -160,7 +167,7 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
160
167
  // === Charger les events SP du jour depuis les agendas groupés ===
161
168
  const dayEvents = await loadAgendaSpEventsForDate(dataDir, yyyymmdd, session);
162
169
  if (dayEvents.length === 0) {
163
- console.warn(`[CRI] [${session}] No agenda SP events found for ${yyyymmdd} → skip split/link`);
170
+ warn(options, `[CRI] [${session}] No agenda SP events found for ${yyyymmdd} → skip split/link`);
164
171
  continue;
165
172
  }
166
173
  // === Lire XML + construire index DOM ===
@@ -175,14 +182,14 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
175
182
  idx = new Map(order.map((el, i) => [el, i]));
176
183
  }
177
184
  catch (e) {
178
- console.warn(`[CRI] [${session}] Cannot read/parse ${f}:`, e);
185
+ warn(options, `[CRI] [${session}] Cannot read/parse ${f}:`, e);
179
186
  continue;
180
187
  }
181
188
  // === Extraire sommaire + matcher vers events agenda ===
182
189
  const blocks = extractSommaireBlocks($, idx);
183
190
  const intervals = buildIntervalsByAgendaEvents($, idx, order, blocks, dayEvents);
184
191
  if (!intervals.length) {
185
- console.warn(`[CRI] [${session}] No confident split intervals for ${yyyymmdd} → skip`);
192
+ warn(options, `[CRI] [${session}] No confident split intervals for ${yyyymmdd} → skip`);
186
193
  continue;
187
194
  }
188
195
  // === Parser / écrire / linker chaque segment par event ===
@@ -191,16 +198,16 @@ export async function retrieveCriXmlDump(dataDir, options = {}) {
191
198
  const outPath = path.join(transformedSessionDir, outName);
192
199
  const cr = await parseCompteRenduIntervalFromFile(xmlPath, iv.startIndex, iv.endIndex, iv.agendaEventId);
193
200
  if (!cr) {
194
- console.warn(`[CRI] [${session}] Empty or no points for ${yyyymmdd} event=${iv.agendaEventId} → skip`);
201
+ warn(options, `[CRI] [${session}] Empty or no points for ${yyyymmdd} event=${iv.agendaEventId} → skip`);
195
202
  continue;
196
203
  }
197
204
  await fs.ensureDir(transformedSessionDir);
198
205
  await fs.writeJSON(outPath, cr, { spaces: 2 });
199
206
  try {
200
- await linkCriEventIntoAgenda(dataDir, yyyymmdd, iv.agendaEventId, cr.uid, cr, session);
207
+ await linkCriEventIntoAgenda(dataDir, yyyymmdd, iv.agendaEventId, cr.uid, cr, session, options);
201
208
  }
202
209
  catch (e) {
203
- console.warn(`[CR] [${session}] Could not link CR into agenda for ${yyyymmdd} event=${iv.agendaEventId}:`, e);
210
+ warn(options, `[CR] [${session}] Could not link CR into agenda for ${yyyymmdd} event=${iv.agendaEventId}:`, e);
204
211
  }
205
212
  }
206
213
  }
@@ -218,7 +225,7 @@ function commitAndPushGit(datasetDir, options) {
218
225
  }
219
226
  }
220
227
  }
221
- async function linkCriEventIntoAgenda(dataDir, yyyymmdd, agendaEventId, crUid, cr, session) {
228
+ async function linkCriEventIntoAgenda(dataDir, yyyymmdd, agendaEventId, crUid, cr, session, options) {
222
229
  const agendadDir = path.join(dataDir, AGENDA_FOLDER, DATA_TRANSFORMED_FOLDER, session.toString());
223
230
  fs.ensureDirSync(agendadDir);
224
231
  const dateISO = `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`;
@@ -230,17 +237,17 @@ async function linkCriEventIntoAgenda(dataDir, yyyymmdd, agendaEventId, crUid, c
230
237
  agenda = await fs.readJSON(agendaPath);
231
238
  }
232
239
  catch (e) {
233
- console.warn(`[CR] unreadable reunion JSON → ${agendaPath} (${e})`);
240
+ warn(options, `[CR] unreadable reunion JSON → ${agendaPath} (${e})`);
234
241
  agenda = null;
235
242
  }
236
243
  }
237
244
  if (!agenda) {
238
- console.warn(`[CR] Missing reunion file for SP event=${agendaEventId}: ${agendaPath}`);
245
+ warn(options, `[CR] Missing reunion file for SP event=${agendaEventId}: ${agendaPath}`);
239
246
  return;
240
247
  }
241
248
  agenda.compteRenduRefUid = crUid;
242
249
  await fs.writeJSON(agendaPath, agenda, { spaces: 2 });
243
- console.log(`[CR] Linked CR ${crUid} → ${path.basename(agendaPath)} (event=${agendaEventId})`);
250
+ log(options, `[CR] Linked CR ${crUid} → ${path.basename(agendaPath)} (event=${agendaEventId})`);
244
251
  }
245
252
  function buildIntervalsByAgendaEvents($, idx, order, blocks, dayEvents) {
246
253
  const MIN_SCORE = 0.65;
@@ -348,9 +355,11 @@ function resolveTargetIndex($, idx, targetId) {
348
355
  }
349
356
  async function main() {
350
357
  const dataDir = assertExistingDirectory(options["dataDir"], "data directory");
351
- console.time("CRI processing time");
358
+ if (!options["silent"])
359
+ console.time("CRI processing time");
352
360
  await retrieveCriXmlDump(dataDir, options);
353
- console.timeEnd("CRI processing time");
361
+ if (!options["silent"])
362
+ console.timeEnd("CRI processing time");
354
363
  }
355
364
  main()
356
365
  .then(() => process.exit(exitCode))
@@ -185,10 +185,49 @@ function runPsqlQuery(command, dataDir, options, connection, stopOnError = true)
185
185
  stdio: ["ignore", "pipe", "pipe"],
186
186
  });
187
187
  }
188
+ function sourceSchemaNames(dataset) {
189
+ if (dataset.database === "sens") {
190
+ return ["sens", "senat"];
191
+ }
192
+ return [dataset.database];
193
+ }
194
+ function inspectSourceDump(dataset, dataDir) {
195
+ const sqlFilePath = path.join(dataDir, `${dataset.database}.sql`);
196
+ if (!fs.existsSync(sqlFilePath)) {
197
+ return {
198
+ copyStatements: 0,
199
+ createTableStatements: 0,
200
+ fileSizeBytes: null,
201
+ schemaQualifiedTableStatements: 0,
202
+ sqlFilePath,
203
+ };
204
+ }
205
+ const sqlContent = fs.readFileSync(sqlFilePath, { encoding: "utf8" });
206
+ const schemaPattern = sourceSchemaNames(dataset)
207
+ .map((schemaName) => schemaName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
208
+ .join("|");
209
+ return {
210
+ copyStatements: (sqlContent.match(/^COPY\s+/gim) ?? []).length,
211
+ createTableStatements: (sqlContent.match(/^CREATE\s+TABLE\s+/gim) ?? []).length,
212
+ fileSizeBytes: fs.statSync(sqlFilePath).size,
213
+ schemaQualifiedTableStatements: (sqlContent.match(new RegExp(`\\b(?:${schemaPattern})\\.`, "g")) ?? []).length,
214
+ sqlFilePath,
215
+ };
216
+ }
188
217
  function ensureStagingSchemaHasTables(dataset, dataDir, options, connection) {
189
218
  const stagingSchema = stagingSchemaName(dataset.database);
190
219
  const tableCount = Number.parseInt(runPsqlQuery(`SELECT count(*) FROM pg_tables WHERE schemaname = '${escapeSqlLiteral(stagingSchema)}'`, dataDir, options, connection).trim(), 10);
220
+ if (tableCount === 0) {
221
+ const dumpInspection = inspectSourceDump(dataset, dataDir);
222
+ const fileSizeLabel = dumpInspection.fileSizeBytes === null ? "missing" : `${dumpInspection.fileSizeBytes} bytes`;
223
+ console.error(`Source dump diagnostics for ${dataset.database}: file=${dumpInspection.sqlFilePath}, ` +
224
+ `size=${fileSizeLabel}, create_table=${dumpInspection.createTableStatements}, ` +
225
+ `copy=${dumpInspection.copyStatements}, ` +
226
+ `schema_refs=${dumpInspection.schemaQualifiedTableStatements}`);
227
+ }
191
228
  assert(tableCount > 0, `Staging schema ${stagingSchema} is empty after importing ${dataset.database}. ` +
229
+ "Source dump may be empty or malformed. " +
230
+ `Check /home/gitlab-runner/senat-data/${dataset.database}.sql and the upstream archive before retrying. ` +
192
231
  `Aborting incremental merge to protect ${senatSchemaName}.`);
193
232
  }
194
233
  function ensureStagingDatabase(dataDir, options, runtime) {
@@ -17,6 +17,10 @@ import { processBisIfNeeded, processOneReunionMatch, writeIfChanged } from "../v
17
17
  const optionsDefinitions = [...commonOptions];
18
18
  const options = commandLineArgs(optionsDefinitions);
19
19
  let exitCode = 10; // 0: some data changed, 10: no modification
20
+ function log(...args) {
21
+ if (!options["silent"])
22
+ console.log(...args);
23
+ }
20
24
  function shouldSkipAgenda(agenda) {
21
25
  if (!agenda.date || !agenda.startTime)
22
26
  return true;
@@ -106,7 +110,6 @@ async function processGroupedReunion(agenda, session, dataDir, lastByVideo) {
106
110
  return;
107
111
  const ctx = await computeContext(agenda, session, dataDir);
108
112
  const skipDownload = shouldSkipDownload(agenda, ctx.baseDir);
109
- let match = null;
110
113
  let best = null;
111
114
  let secondBest = null;
112
115
  // 2) Match + download artifacts (only if not skipped)
@@ -114,12 +117,12 @@ async function processGroupedReunion(agenda, session, dataDir, lastByVideo) {
114
117
  STATS.total++;
115
118
  const candidates = await fetchCandidatesForAgenda(agenda, options);
116
119
  if (!candidates) {
117
- console.log(`[warn] ${agenda.uid} No candidate found for this reunion. Probably VOD not published yet.`);
120
+ log(`[warn] ${agenda.uid} No candidate found for this reunion. Probably VOD not published yet.`);
118
121
  return;
119
122
  }
120
- match = await matchAgendaToVideo({ agenda, agendaTs: ctx.agendaTs, candidates, options });
123
+ const match = await matchAgendaToVideo({ agenda, agendaTs: ctx.agendaTs, candidates, options });
121
124
  if (!match) {
122
- console.log(`[miss] ${agenda.uid} No match found for this reunion`);
125
+ log(`[miss] ${agenda.uid} No match found for this reunion`);
123
126
  return;
124
127
  }
125
128
  ;
@@ -128,8 +131,7 @@ async function processGroupedReunion(agenda, session, dataDir, lastByVideo) {
128
131
  await writeMatchArtifacts({ agenda, ctx, best, secondBest });
129
132
  }
130
133
  if (best && isAmbiguousTimeOriginal(agenda.events[0].timeOriginal)) {
131
- if (!options["silent"])
132
- console.log("If the time is ambiguous, update agenda startTime from matched video");
134
+ log("If the time is ambiguous, update agenda startTime from matched video");
133
135
  agenda = { ...agenda, startTime: epochToParisDateTime(best.epoch)?.startTime ?? agenda.startTime };
134
136
  }
135
137
  // 3) Always update BEST agenda JSON from local NVS
@@ -159,7 +161,7 @@ async function processGroupedReunion(agenda, session, dataDir, lastByVideo) {
159
161
  });
160
162
  }
161
163
  async function processAll(dataDir, sessions) {
162
- console.log("Process all Agendas and fetch video's url");
164
+ log("Process all Agendas and fetch video's url");
163
165
  for (const session of sessions) {
164
166
  const lastByVideo = new Map();
165
167
  for (const { item: agenda } of iterLoadSenatAgendas(dataDir, session)) {
@@ -187,12 +189,14 @@ async function main() {
187
189
  const dataDir = assertExistingDirectory(options["dataDir"], "data directory");
188
190
  const sessions = getSessionsFromStart((options["fromSession"] ?? UNDEFINED_SESSION));
189
191
  const TIMER = "senat-agendas→videos processing time";
190
- console.time(TIMER);
192
+ if (!options["silent"])
193
+ console.time(TIMER);
191
194
  await processAll(dataDir, sessions);
192
- console.timeEnd(TIMER);
195
+ if (!options["silent"])
196
+ console.timeEnd(TIMER);
193
197
  const { total, accepted } = STATS;
194
198
  const ratio = total ? ((accepted / total) * 100).toFixed(1) : "0.0";
195
- console.log(`[summary] accepted=${accepted} / total=${total} (${ratio}%)`);
199
+ log(`[summary] accepted=${accepted} / total=${total} (${ratio}%)`);
196
200
  }
197
201
  if (import.meta.url === pathToFileURL(process.argv[1]).href) {
198
202
  main()
@@ -283,6 +283,13 @@ BEGIN
283
283
  )
284
284
  INTO use_row_multiset_merge;
285
285
 
286
+ -- The synchronization below temporarily drops primary/unique constraints before
287
+ -- issuing DELETE/UPDATE statements. On tables that belong to a publication with
288
+ -- deletes/updates enabled, PostgreSQL then refuses those statements unless the
289
+ -- table has an explicit replica identity. FULL keeps logical replication valid
290
+ -- while the constraints are being rebuilt.
291
+ EXECUTE format('ALTER TABLE ${senatSchemaName}.%I REPLICA IDENTITY FULL', table_row.tablename);
292
+
286
293
  FOR constraint_row IN
287
294
  SELECT con.conname
288
295
  FROM pg_constraint con
@@ -16,6 +16,7 @@ export async function* findAllAmendements(fromSession) {
16
16
  .leftJoin("ameli_lec_ameli as lec_ameli", "lec_ameli.id", "txt_ameli.lecid")
17
17
  .leftJoin("dosleg_texte as texte", (join) => join.onRef("ses.ann", "=", "texte.sesann").onRef("txt_ameli.numabs", "=", "texte.texnum"))
18
18
  .leftJoin("dosleg_lecass as lecass", "lecass.lecassidt", "texte.lecassidt")
19
+ .leftJoin("dosleg_org as org_lecass", "org_lecass.orgcod", "lecass.orgcod")
19
20
  .leftJoin("ameli_mot as mot", "mot.id", "amd.motid")
20
21
  .leftJoin("ameli_avicom as avicom", "avicom.id", "amd.avcid")
21
22
  .leftJoin("ameli_avigvt as avigvt", "avigvt.id", "amd.avgid")
@@ -107,12 +108,11 @@ export async function* findAllAmendements(fromSession) {
107
108
  )
108
109
  `.as("url"),
109
110
  "grppol_ameli.lilcou as au_nom_de_groupe_politique",
110
- sql `rtrim(${eb.ref("com_ameli.lil")})`.as("au_nom_de_commission"),
111
- sql `rtrim(${eb.ref("com_ameli.cod")})`.as("code_commission"),
111
+ sql `coalesce(rtrim(${eb.ref("com_ameli.lil")}), ${eb.ref("org_lecass.orgnom")})`.as("au_nom_de_commission"),
112
+ sql `coalesce(rtrim(${eb.ref("com_ameli.cod")}), ${eb.ref("org_lecass.senorgcod")})`.as("code_commission"),
112
113
  sql `(${eb.ref("cab.entid")} is not null)`.as("auteur_est_gouvernement"),
113
114
  // Sous-requête scalaire: numéro de scrutin
114
115
  eb
115
- .withSchema("senat")
116
116
  .selectFrom("dosleg_amescr as amescr")
117
117
  .leftJoin("dosleg_scr as scr", (join) => join.onRef("amescr.scrnum", "=", "scr.scrnum").onRef("amescr.sesann", "=", "scr.sesann"))
118
118
  .leftJoin("dosleg_date_seance as date_seance", "date_seance.code", "scr.code")
@@ -124,7 +124,6 @@ export async function* findAllAmendements(fromSession) {
124
124
  .as("scrutin_num"),
125
125
  // Sous-requête pour les auteurs
126
126
  jsonArrayFrom(eb
127
- .withSchema("senat")
128
127
  .selectFrom("ameli_amdsen as amdsen")
129
128
  .leftJoin("ameli_sen_ameli as sen_ameli", "sen_ameli.entid", "amdsen.senid")
130
129
  .leftJoin("ameli_grppol_ameli as grppol_ameli2", "grppol_ameli2.entid", "amdsen.grpid")
@@ -218,7 +218,7 @@ p.has-alinea {
218
218
  else if (child.nodeType === 1) {
219
219
  const element = child;
220
220
  const tagName = element.tagName.toLowerCase();
221
- let htmlElement = null;
221
+ let htmlElement;
222
222
  switch (tagName) {
223
223
  case "article": {
224
224
  htmlElement = htmlDoc.createElement("div");
@@ -13,7 +13,6 @@ export async function* findAll() {
13
13
  "debats.debsyn as etat_synchronisation",
14
14
  // Sections principales (PJL)
15
15
  jsonArrayFrom(eb
16
- .withSchema("senat")
17
16
  .selectFrom("debats_secdis as secdis")
18
17
  .leftJoin("debats_typsec as typsec", "typsec.typseccod", "secdis.typseccod")
19
18
  .select((eb2) => [
@@ -26,7 +25,6 @@ export async function* findAll() {
26
25
  "secdis.lecassidt as lecture_id",
27
26
  // Interventions de la section
28
27
  jsonArrayFrom(eb2
29
- .withSchema("senat")
30
28
  .selectFrom("debats_intpjl as intpjl")
31
29
  .leftJoin("dosleg_auteur as auteur", "auteur.autcod", "intpjl.autcod")
32
30
  .select([
@@ -49,7 +47,6 @@ export async function* findAll() {
49
47
  .orderBy("secdis.secdisordid", "asc")).as("sections"),
50
48
  // Sections diverses
51
49
  jsonArrayFrom(eb
52
- .withSchema("senat")
53
50
  .selectFrom("debats_secdivers as secdivers")
54
51
  .leftJoin("debats_typsec as typsec", "typsec.typseccod", "secdivers.typseccod")
55
52
  .select((eb2) => [
@@ -59,7 +56,6 @@ export async function* findAll() {
59
56
  "typsec.typseccat as categorie",
60
57
  // Interventions diverses
61
58
  jsonArrayFrom(eb2
62
- .withSchema("senat")
63
59
  .selectFrom("debats_intdivers as intdivers")
64
60
  .leftJoin("dosleg_auteur as auteur", "auteur.autcod", "intdivers.autcod")
65
61
  .select([
@@ -81,7 +77,6 @@ export async function* findAll() {
81
77
  .whereRef("secdivers.datsea", "=", "debats.datsea")).as("sections_divers"),
82
78
  // Lectures
83
79
  jsonArrayFrom(eb
84
- .withSchema("senat")
85
80
  .selectFrom("debats_lecassdeb as lecassdeb")
86
81
  .select(["lecassdeb.lecassidt as id"])
87
82
  .whereRef("lecassdeb.datsea", "=", "debats.datsea")).as("lectures"),
@@ -36,7 +36,6 @@ export async function* findAllTextes() {
36
36
  sql `${eb.ref("texte.sesann")}::int`.as("session"),
37
37
  // Auteurs
38
38
  jsonArrayFrom(eb
39
- .withSchema("senat")
40
39
  .selectFrom("dosleg_auteur as auteur")
41
40
  .leftJoin("dosleg_ecr as ecr", "ecr.autcod", "auteur.autcod")
42
41
  .leftJoin("dosleg_rolsig as rolsig", "rolsig.signataire", "ecr.signataire")
@@ -95,7 +94,6 @@ export async function* findAllRapports() {
95
94
  sql `${eb.ref("rap.sesann")}::int`.as("session"),
96
95
  // Auteurs
97
96
  jsonArrayFrom(eb
98
- .withSchema("senat")
99
97
  .selectFrom("dosleg_auteur as auteur")
100
98
  .leftJoin("dosleg_ecr as ecr", "ecr.autcod", "auteur.autcod")
101
99
  .leftJoin("dosleg_rolsig as rolsig", "rolsig.signataire", "ecr.signataire")
@@ -111,7 +109,6 @@ export async function* findAllRapports() {
111
109
  .whereRef("ecr.rapcod", "=", "rap.rapcod")).as("auteurs"),
112
110
  // Documents annexes
113
111
  jsonArrayFrom(eb
114
- .withSchema("senat")
115
112
  .selectFrom("dosleg_docatt as docatt")
116
113
  .leftJoin("dosleg_typatt as typatt", "typatt.typattcod", "docatt.typattcod")
117
114
  .select(["docatt.docatturl as url", "typatt.typattlib as type_document"])
@@ -58,7 +58,6 @@ export async function* findAllDossiers() {
58
58
  "loi.url_jo3 as url_JO_correctif_2",
59
59
  // Lectures
60
60
  jsonArrayFrom(eb
61
- .withSchema("senat")
62
61
  .selectFrom("dosleg_lecture as lecture")
63
62
  .leftJoin("dosleg_typlec as typlec", "typlec.typleccod", "lecture.typleccod")
64
63
  .select((eb2) => [
@@ -68,7 +67,6 @@ export async function* findAllDossiers() {
68
67
  "typlec.typlecord as ordre_lecture_num",
69
68
  // Lectures par assemblée
70
69
  jsonArrayFrom(eb2
71
- .withSchema("senat")
72
70
  .selectFrom("dosleg_lecass as lecass")
73
71
  .leftJoin("dosleg_ass as ass", "ass.codass", "lecass.codass")
74
72
  .leftJoin("dosleg_org as org", "org.orgcod", "lecass.orgcod")
@@ -114,7 +112,6 @@ export async function* findAllDossiers() {
114
112
  sql `lecass.lecassamecomadoses::int`.as("session_liasse_amendements_adoptes_commission"),
115
113
  // Textes
116
114
  jsonArrayFrom(eb3
117
- .withSchema("senat")
118
115
  .selectFrom("dosleg_texte as texte")
119
116
  .leftJoin("dosleg_oritxt as oritxt", "oritxt.oritxtcod", "texte.oritxtcod")
120
117
  .leftJoin("dosleg_typtxt as typtxt", "typtxt.typtxtcod", "texte.typtxtcod")
@@ -158,7 +155,6 @@ export async function* findAllDossiers() {
158
155
  "texte.prix as prix",
159
156
  // Auteurs du texte
160
157
  jsonArrayFrom(eb4
161
- .withSchema("senat")
162
158
  .selectFrom("dosleg_auteur as auteur")
163
159
  .leftJoin("dosleg_ecr as ecr", "ecr.autcod", "auteur.autcod")
164
160
  .leftJoin("dosleg_rolsig as rolsig", "rolsig.signataire", "ecr.signataire")
@@ -178,7 +174,6 @@ export async function* findAllDossiers() {
178
174
  .orderBy(sql `array_position(array['0','2','1'], oritxt.oriordre)`)).as("textes"),
179
175
  // Rapports
180
176
  jsonArrayFrom(eb3
181
- .withSchema("senat")
182
177
  .selectFrom("dosleg_rap as rap")
183
178
  .leftJoin("dosleg_raporg as raporg", "raporg.rapcod", "rap.rapcod")
184
179
  .leftJoin("dosleg_denrap as denrap", "denrap.coddenrap", "rap.coddenrap")
@@ -228,7 +223,6 @@ export async function* findAllDossiers() {
228
223
  "lecassrap.lecassrapord as ordre_rapport",
229
224
  // Auteurs du rapport
230
225
  jsonArrayFrom(eb4
231
- .withSchema("senat")
232
226
  .selectFrom("dosleg_auteur as auteur")
233
227
  .leftJoin("dosleg_ecr as ecr", "ecr.autcod", "auteur.autcod")
234
228
  .leftJoin("dosleg_rolsig as rolsig", "rolsig.signataire", "ecr.signataire")
@@ -244,7 +238,6 @@ export async function* findAllDossiers() {
244
238
  .whereRef("ecr.rapcod", "=", "rap.rapcod")).as("auteurs"),
245
239
  // Documents annexes
246
240
  jsonArrayFrom(eb4
247
- .withSchema("senat")
248
241
  .selectFrom("dosleg_docatt as docatt")
249
242
  .leftJoin("dosleg_typatt as typatt", "typatt.typattcod", "docatt.typattcod")
250
243
  .select(["docatt.docatturl as url", "typatt.typattlib as type_document"])
@@ -253,7 +246,6 @@ export async function* findAllDossiers() {
253
246
  .whereRef("lecassrap.lecassidt", "=", "lecass.lecassidt")).as("rapports"),
254
247
  // Dates de séances
255
248
  jsonArrayFrom(eb3
256
- .withSchema("senat")
257
249
  .selectFrom("dosleg_date_seance as date_seance")
258
250
  .select([
259
251
  "date_seance.code as code",
@@ -264,7 +256,6 @@ export async function* findAllDossiers() {
264
256
  // Commissions saisies (source : ameli.sai)
265
257
  // Fusionnées avec la commission au fond (lecass.orgcod) en post-traitement
266
258
  jsonArrayFrom(eb3
267
- .withSchema("senat")
268
259
  .selectFrom("ameli_sai as sai")
269
260
  .innerJoin("ameli_txt_ameli as txt_a", "txt_a.id", "sai.txtid")
270
261
  .innerJoin("ameli_com_ameli as com_a", "com_a.entid", "sai.comid")
@@ -285,7 +276,6 @@ export async function* findAllDossiers() {
285
276
  .groupBy(["com_a.lib", "com_a.lil", "sai.saityp", "sai.isdelegfond"])).as("commissions_saisies_ameli"),
286
277
  // Réunions de commission (source : dosleg_aud)
287
278
  jsonArrayFrom(eb3
288
- .withSchema("senat")
289
279
  .selectFrom("dosleg_aud as aud")
290
280
  .innerJoin("dosleg_org as org_a", "org_a.orgcod", "aud.orgcod")
291
281
  .select([
@@ -305,7 +295,6 @@ export async function* findAllDossiers() {
305
295
  .orderBy(sql `COALESCE(typlec.typlecord::int, 9999)`, "asc")).as("lectures"),
306
296
  // Thèmes
307
297
  jsonArrayFrom(eb
308
- .withSchema("senat")
309
298
  .selectFrom("dosleg_the as the")
310
299
  .leftJoin("dosleg_loithe as loithe", "loithe.thecle", "the.thecle")
311
300
  .select(["the.thelib as libelle"])
@@ -63,7 +63,6 @@ export async function* findAll() {
63
63
  )`.as("themes"),
64
64
  // Réponses
65
65
  jsonArrayFrom(eb
66
- .withSchema("senat")
67
66
  .selectFrom("questions_tam_reponses as tam_reponses")
68
67
  .select([
69
68
  sql `to_char(tam_reponses.datejorep, 'YYYY-MM-DD')`.as("date_reponse_JO"),
@@ -33,7 +33,6 @@ export async function* findAllScrutins(fromSession) {
33
33
  sql `${eb.ref("scr.scrpousea")}::text`.as("nombre_pour_seance"),
34
34
  // Votes individuels
35
35
  jsonArrayFrom(eb
36
- .withSchema("senat")
37
36
  .selectFrom("dosleg_votsen as votsen")
38
37
  .leftJoin("dosleg_titsen as titsen", "titsen.titsencod", "votsen.titsencod")
39
38
  .leftJoin("dosleg_stavot as stavot", "stavot.stavotidt", "votsen.stavotidt")
@@ -57,9 +56,7 @@ export async function* findAllScrutins(fromSession) {
57
56
  .orderBy("memgrppol.memgrppoldatdeb", (ob) => ob.desc().nullsLast())).as("votes"),
58
57
  // Groupes votants (avec déduplication via window function)
59
58
  jsonArrayFrom(eb
60
- .withSchema("senat")
61
59
  .selectFrom(eb
62
- .withSchema("senat")
63
60
  .selectFrom("dosleg_votsen as votsen")
64
61
  .leftJoin("dosleg_posvot as posvot", "posvot.posvotcod", "votsen.posvotcod")
65
62
  .leftJoin("dosleg_stavot as stavot", "stavot.stavotidt", "votsen.stavotidt")
@@ -93,7 +90,6 @@ export async function* findAllScrutins(fromSession) {
93
90
  .groupBy("unique_votes.grppolcod")).as("groupes_votants"),
94
91
  // Mises au point (corrections)
95
92
  jsonArrayFrom(eb
96
- .withSchema("senat")
97
93
  .selectFrom("dosleg_corscr as corscr")
98
94
  .select([
99
95
  "corscr.corscrtxt as texte",
@@ -37,14 +37,12 @@ export async function* findAll() {
37
37
  "sen.sendaiurl as url_hatvp",
38
38
  // URLs
39
39
  jsonArrayFrom(eb
40
- .withSchema("senat")
41
40
  .selectFrom("sens_senurl as senurl")
42
41
  .select(["senurl.typurlcod as code_url", "senurl.senurlurl as url", "senurl.senurlnumtri as order_num"])
43
42
  .whereRef("senurl.senmat", "=", "sen.senmat")
44
43
  .orderBy("senurl.senurlnumtri", "asc")).as("urls"),
45
44
  // Mandats sénatoriaux
46
45
  jsonArrayFrom(eb
47
- .withSchema("senat")
48
46
  .selectFrom("sens_elusen as elusen")
49
47
  .leftJoin("sens_etadebman as etadebman", "etadebman.etadebmancod", "elusen.etadebmancod")
50
48
  .leftJoin("sens_etafinman as etafinman", "etafinman.etafinmancod", "elusen.etafinmancod")
@@ -61,7 +59,6 @@ export async function* findAll() {
61
59
  .orderBy("elusen.eludatdeb", (ob) => ob.desc().nullsLast())).as("mandats_senateur"),
62
60
  // Commissions
63
61
  jsonArrayFrom(eb
64
- .withSchema("senat")
65
62
  .selectFrom("sens_memcom as memcom")
66
63
  .leftJoin("sens_com as com2", "com2.orgcod", "memcom.orgcod")
67
64
  .leftJoin("sens_typorg as typorg", "typorg.typorgcod", "com2.typorgcod")
@@ -76,7 +73,6 @@ export async function* findAll() {
76
73
  "memcom.memcomdatdeb as order_date",
77
74
  // Fonctions dans la commission
78
75
  jsonArrayFrom(eb2
79
- .withSchema("senat")
80
76
  .selectFrom("sens_fonmemcom as fonmemcom")
81
77
  .leftJoin("sens_foncom as foncom", "foncom.foncomcod", "fonmemcom.foncomcod")
82
78
  .select([
@@ -95,7 +91,6 @@ export async function* findAll() {
95
91
  .orderBy("memcom.memcomdatdeb", (ob) => ob.desc().nullsLast())).as("commissions"),
96
92
  // Délégations
97
93
  jsonArrayFrom(eb
98
- .withSchema("senat")
99
94
  .selectFrom("sens_memdelega as memdelega")
100
95
  .leftJoin("sens_delega as delega", "delega.orgcod", "memdelega.orgcod")
101
96
  .leftJoin("sens_designorg as designorg", "designorg.designcod", "memdelega.designcod")
@@ -111,7 +106,6 @@ export async function* findAll() {
111
106
  "memdelega.memdelegadatdeb as order_date",
112
107
  // Fonctions dans la délégation
113
108
  jsonArrayFrom(eb2
114
- .withSchema("senat")
115
109
  .selectFrom("sens_fonmemdelega as fonmemdelega")
116
110
  .leftJoin("sens_fondelega as fondelega", "fondelega.fondelcod", "fonmemdelega.fondelcod")
117
111
  .select([
@@ -130,7 +124,6 @@ export async function* findAll() {
130
124
  .orderBy("memdelega.memdelegadatdeb", (ob) => ob.desc().nullsLast())).as("delegations"),
131
125
  // Groupes politiques
132
126
  jsonArrayFrom(eb
133
- .withSchema("senat")
134
127
  .selectFrom("sens_memgrppol as memgrppol")
135
128
  .leftJoin("sens_grppol as grppol2", "grppol2.grppolcod", "memgrppol.grppolcod")
136
129
  .leftJoin("sens_typapppol as typapppol", "typapppol.typapppolcod", "memgrppol.typapppolcod")
@@ -146,7 +139,6 @@ export async function* findAll() {
146
139
  "memgrppol.memgrppoldatdeb as order_date",
147
140
  // Fonctions dans le groupe politique
148
141
  jsonArrayFrom(eb2
149
- .withSchema("senat")
150
142
  .selectFrom("sens_fonmemgrppol as fonmemgrppol")
151
143
  .leftJoin("sens_fongrppol as fongrppol", "fongrppol.fongrppolcod", "fonmemgrppol.fongrppolcod")
152
144
  .select([
@@ -165,7 +157,6 @@ export async function* findAll() {
165
157
  .orderBy("memgrppol.memgrppoldatdeb", (ob) => ob.desc().nullsLast())).as("groupes"),
166
158
  // Organismes divers (groupes d'études, missions, etc.)
167
159
  jsonArrayFrom(eb
168
- .withSchema("senat")
169
160
  .selectFrom("sens_memorg as memorg")
170
161
  .leftJoin("sens_org as org", "org.orgcod", "memorg.orgcod")
171
162
  .leftJoin("sens_typorg as typorg4", "typorg4.typorgcod", "org.typorgcod")
@@ -180,7 +171,6 @@ export async function* findAll() {
180
171
  "memorg.memorgdatdeb as order_date",
181
172
  // Fonctions dans l'organisme
182
173
  jsonArrayFrom(eb2
183
- .withSchema("senat")
184
174
  .selectFrom("sens_fonmemorg as fonmemorg")
185
175
  .leftJoin("sens_fonorg as fonorg", "fonorg.fonorgcod", "fonmemorg.fonorgcod")
186
176
  .select([
@@ -199,9 +189,7 @@ export async function* findAll() {
199
189
  .orderBy("memorg.memorgdatdeb", (ob) => ob.desc().nullsLast())).as("organismes"),
200
190
  // Groupes sénatoriaux (groupes d'amitié)
201
191
  jsonArrayFrom(eb
202
- .withSchema("senat")
203
192
  .selectFrom(eb
204
- .withSchema("senat")
205
193
  .selectFrom("sens_memgrpsen as memgrpsen_raw")
206
194
  .select([
207
195
  "memgrpsen_raw.orgcod as orgcod",
@@ -231,7 +219,6 @@ export async function* findAll() {
231
219
  "memgrpsen.memgrpsendatent as order_date",
232
220
  // Fonctions dans le groupe sénatorial
233
221
  jsonArrayFrom(eb2
234
- .withSchema("senat")
235
222
  .selectFrom("sens_fonmemgrpsen as fonmemgrpsen")
236
223
  .leftJoin("sens_fongrpsen as fongrpsen", "fongrpsen.fongrpsencod", "fonmemgrpsen.fongrpsencod")
237
224
  .select([
@@ -249,7 +236,6 @@ export async function* findAll() {
249
236
  .orderBy("memgrpsen.memgrpsendatent", (ob) => ob.desc().nullsLast())).as("groupes_senatoriaux"),
250
237
  // Fonctions au bureau du Sénat
251
238
  jsonArrayFrom(eb
252
- .withSchema("senat")
253
239
  .selectFrom("sens_senbur as senbur")
254
240
  .leftJoin("sens_bur as bur", "bur.burcod", "senbur.burcod")
255
241
  .select([
@@ -266,7 +252,6 @@ export async function* findAll() {
266
252
  .orderBy("senbur.senburdatdeb", (ob) => ob.desc().nullsLast())).as("fonctions_bureau"),
267
253
  // Points de contact
268
254
  jsonArrayFrom(eb
269
- .withSchema("senat")
270
255
  .selectFrom("sens_poicon as poicon")
271
256
  .select((eb2) => [
272
257
  sql `poicon.poiconid::text`.as("id"),
@@ -275,7 +260,6 @@ export async function* findAll() {
275
260
  "poicon.poiconnumtri as order_num",
276
261
  // Adresses
277
262
  jsonArrayFrom(eb2
278
- .withSchema("senat")
279
263
  .selectFrom("sens_adresse as adresse")
280
264
  .select([
281
265
  "adresse.adrnumvoi as numero_voie",
@@ -293,7 +277,6 @@ export async function* findAll() {
293
277
  .orderBy("adresse.adrnumtri", "asc")).as("adresses"),
294
278
  // Téléphones
295
279
  jsonArrayFrom(eb2
296
- .withSchema("senat")
297
280
  .selectFrom("sens_telephone as telephone")
298
281
  .select([
299
282
  "telephone.typtelcod as type",
@@ -265,10 +265,6 @@ function computeCodeEtape(ev, dossier, flatActes) {
265
265
  return cleanCode(genericActe.codeActe);
266
266
  }
267
267
  }
268
- console.log(`✖ No stage code found for ev=${ev.id} (Date: ${evDate}, Nature: ${nature}, Lecture: ${lecture})`, {
269
- totalActsInDossier: dossier["actes_legislatifs"]?.length || 0,
270
- firstActDate: flat[0]?.date,
271
- });
272
268
  return null;
273
269
  }
274
270
  function buildFlatActes(dossier) {
@@ -114,7 +114,9 @@ export async function matchOneReunion(args) {
114
114
  if (isSP && timeAmbigious) {
115
115
  // Optional safety: second must also be "acceptable"
116
116
  if (second.score >= minAccept) {
117
- console.log(" - special case: séance publique + time ambigious => accepting best + secondBest (bis)");
117
+ if (!options["silent"]) {
118
+ console.log(" - special case: séance publique + time ambigious => accepting best + secondBest (bis)");
119
+ }
118
120
  return { best, secondBest: second, reason: "margin_ambiguous_time_sp" };
119
121
  }
120
122
  }
@@ -5,6 +5,14 @@ import { fetchText } from "./search.js";
5
5
  import fs from "fs-extra";
6
6
  import fsp from "fs/promises";
7
7
  import path from "path";
8
+ function log(options, ...args) {
9
+ if (!options["silent"])
10
+ console.log(...args);
11
+ }
12
+ function warn(options, ...args) {
13
+ if (!options["silent"])
14
+ console.warn(...args);
15
+ }
8
16
  function isReunion(value) {
9
17
  return typeof value === "object" && value !== null && "uid" in value;
10
18
  }
@@ -18,18 +26,18 @@ export async function processOneReunionMatch(args) {
18
26
  finalTxt = await fsp.readFile(path.join(baseDir, "finalplayer.nvs"), "utf-8");
19
27
  }
20
28
  catch {
21
- console.warn(`[skip] Missing NVS files for reunion ${reunionUid}`);
29
+ warn(options, `[skip] Missing NVS files for reunion ${reunionUid}`);
22
30
  return;
23
31
  }
24
32
  const master = buildSenatVodMasterM3u8FromNvs(dataTxt);
25
33
  if (!master) {
26
- console.warn(`[warn] Cannot build m3u8 for reunion ${reunionUid}`);
34
+ warn(options, `[warn] Cannot build m3u8 for reunion ${reunionUid}`);
27
35
  return;
28
36
  }
29
37
  const agendaJsonPath = path.join(dataDir, AGENDA_FOLDER, DATA_TRANSFORMED_FOLDER, String(session), `${agenda.uid}.json`);
30
38
  // Ensure it exists first.
31
39
  if (!(await fs.pathExists(agendaJsonPath))) {
32
- console.warn(`[warn] agenda file not found: ${agendaJsonPath}`);
40
+ warn(options, `[warn] agenda file not found: ${agendaJsonPath}`);
33
41
  return;
34
42
  }
35
43
  let timecodeDebutVideo = null;
@@ -46,7 +54,7 @@ export async function processOneReunionMatch(args) {
46
54
  if (prev && prev.agendaJsonPath !== agendaJsonPath) {
47
55
  // micro-safety: do not close with an earlier timecode
48
56
  if (timecodeDebutVideo <= prev.start) {
49
- console.warn("[warn] timecode order inversion on same video: " +
57
+ warn(options, "[warn] timecode order inversion on same video: " +
50
58
  `prev=${prev.agendaUid}(${prev.start}s) -> ` +
51
59
  `cur=${agenda.uid}(${timecodeDebutVideo}s). ` +
52
60
  "Skip closing prev to avoid negative segment.");
@@ -55,6 +63,7 @@ export async function processOneReunionMatch(args) {
55
63
  await patchAgendaTimecodeFin({
56
64
  agendaJsonPath: prev.agendaJsonPath,
57
65
  timecodeFinVideo: timecodeDebutVideo,
66
+ options,
58
67
  writeIfChanged,
59
68
  });
60
69
  }
@@ -68,11 +77,11 @@ export async function processOneReunionMatch(args) {
68
77
  obj = JSON.parse(raw);
69
78
  }
70
79
  catch {
71
- console.warn(`[warn] invalid JSON in ${agendaJsonPath}`);
80
+ warn(options, `[warn] invalid JSON in ${agendaJsonPath}`);
72
81
  return;
73
82
  }
74
83
  if (!isReunion(obj)) {
75
- console.warn(`[warn] invalid reunion payload in ${agendaJsonPath}`);
84
+ warn(options, `[warn] invalid reunion payload in ${agendaJsonPath}`);
76
85
  return;
77
86
  }
78
87
  const next = { ...obj, urlVideo: master, startTime: agenda.startTime, urlPageVideo: best?.pageUrl };
@@ -84,10 +93,8 @@ export async function processOneReunionMatch(args) {
84
93
  delete next.timecodeFinVideo;
85
94
  }
86
95
  await writeIfChanged(agendaJsonPath, JSON.stringify(next, null, 2));
87
- if (!options["silent"]) {
88
- console.log(`[write] ${agenda.uid} urlVideo ← ${master}` +
89
- (timecodeDebutVideo != null ? ` (timecodeDebutVideo ← ${timecodeDebutVideo}s)` : ""));
90
- }
96
+ log(options, `[write] ${agenda.uid} urlVideo ← ${master}` +
97
+ (timecodeDebutVideo != null ? ` (timecodeDebutVideo ← ${timecodeDebutVideo}s)` : ""));
91
98
  }
92
99
  export async function processBisIfNeeded(args) {
93
100
  const { agenda, secondBest, ctx, skipDownload, options, lastByVideo, writeIfChanged, processOneReunionMatch, getAgendaSegmentTimecodes, buildSenatVodMasterM3u8FromNvs, } = args;
@@ -106,7 +113,7 @@ export async function processBisIfNeeded(args) {
106
113
  };
107
114
  const baseDirBis = path.join(path.dirname(ctx.baseDir), bisUid);
108
115
  await fs.ensureDir(baseDirBis);
109
- await ensureBisAgendaJson({ agenda, agendaBis, dataDir: ctx.dataDir, session: ctx.session });
116
+ await ensureBisAgendaJson({ agenda, agendaBis, dataDir: ctx.dataDir, session: ctx.session, options });
110
117
  await downloadNvsForMatch(secondBest, baseDirBis);
111
118
  await processOneReunionMatch({
112
119
  agenda: agendaBis,
@@ -122,26 +129,26 @@ export async function processBisIfNeeded(args) {
122
129
  });
123
130
  }
124
131
  async function ensureBisAgendaJson(args) {
125
- const { agenda, agendaBis, dataDir, session } = args;
132
+ const { agenda, agendaBis, dataDir, session, options } = args;
126
133
  const agendaJsonPath = path.join(dataDir, AGENDA_FOLDER, DATA_TRANSFORMED_FOLDER, String(session), `${agenda.uid}.json`);
127
134
  const agendaBisJsonPath = path.join(dataDir, AGENDA_FOLDER, DATA_TRANSFORMED_FOLDER, String(session), `${agendaBis.uid}.json`);
128
135
  if (!(await fs.pathExists(agendaJsonPath))) {
129
- console.warn(`[bis] original agenda json not found to clone: ${agendaJsonPath}`);
136
+ warn(options, `[bis] original agenda json not found to clone: ${agendaJsonPath}`);
130
137
  return;
131
138
  }
132
139
  try {
133
140
  const raw = await fsp.readFile(agendaJsonPath, "utf-8");
134
141
  const obj = JSON.parse(raw);
135
142
  if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
136
- console.warn(`[bis] cannot clone agenda json: expected object in ${agendaJsonPath}, ` +
143
+ warn(options, `[bis] cannot clone agenda json: expected object in ${agendaJsonPath}, ` +
137
144
  `got ${Array.isArray(obj) ? "array" : typeof obj}`);
138
145
  return;
139
146
  }
140
147
  await writeIfChanged(agendaBisJsonPath, JSON.stringify(agendaBis, null, 2));
141
- console.log(`[bis] created agenda json ${agendaBis.uid}.json from ${agenda.uid}.json`);
148
+ log(options, `[bis] created agenda json ${agendaBis.uid}.json from ${agenda.uid}.json`);
142
149
  }
143
150
  catch (e) {
144
- console.warn(`[bis] cannot clone agenda json ${agenda.uid}.json -> ${agendaBis.uid}.json:`, e?.message);
151
+ warn(options, `[bis] cannot clone agenda json ${agenda.uid}.json -> ${agendaBis.uid}.json:`, e?.message);
145
152
  }
146
153
  }
147
154
  async function downloadNvsForMatch(best, baseDir) {
@@ -164,7 +171,7 @@ export async function writeIfChanged(p, content) {
164
171
  await fsp.writeFile(p, content, "utf-8");
165
172
  }
166
173
  async function patchAgendaTimecodeFin(args) {
167
- const { agendaJsonPath, timecodeFinVideo, writeIfChanged } = args;
174
+ const { agendaJsonPath, timecodeFinVideo, options, writeIfChanged } = args;
168
175
  if (!(await fs.pathExists(agendaJsonPath)))
169
176
  return;
170
177
  const raw = await fsp.readFile(agendaJsonPath, "utf-8");
@@ -173,11 +180,11 @@ async function patchAgendaTimecodeFin(args) {
173
180
  obj = JSON.parse(raw);
174
181
  }
175
182
  catch {
176
- console.warn(`[warn] invalid JSON in ${agendaJsonPath}`);
183
+ warn(options, `[warn] invalid JSON in ${agendaJsonPath}`);
177
184
  return;
178
185
  }
179
186
  if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
180
- console.warn(`[warn] invalid reunion payload in ${agendaJsonPath}`);
187
+ warn(options, `[warn] invalid reunion payload in ${agendaJsonPath}`);
181
188
  return;
182
189
  }
183
190
  const next = { ...obj, timecodeFinVideo };
@@ -33,6 +33,7 @@ describe("incremental import SQL", () => {
33
33
  expect(sql).toContain("t.%1$I IS NOT DISTINCT FROM s.%1$I");
34
34
  expect(sql).toContain("jsonb_build_array(");
35
35
  expect(sql).toContain("Sync strategy for %.%: %");
36
+ expect(sql).toContain("ALTER TABLE senat.%I REPLICA IDENTITY FULL");
36
37
  expect(sql).toContain("Rejected sync key for %.% from % because only %/% columns are present in both staging and target: %");
37
38
  expect(sql).toContain("WHERE table_namespace.nspname IN ('dosleg_staging', 'senat')");
38
39
  expect(sql).toContain("CASE WHEN table_namespace.nspname = 'dosleg_staging' THEN 0 ELSE 1 END AS schema_priority");
@@ -42,7 +43,9 @@ describe("incremental import SQL", () => {
42
43
  expect(sql).toContain("LEFT JOIN target_counts reference_row USING (row_signature)");
43
44
  expect(sql).toContain("DELETE FROM senat.%1$I t WHERE NOT EXISTS (SELECT 1 FROM %2$I.%1$I s WHERE %3$s)");
44
45
  expect(sql).toContain("UPDATE senat.%1$I t SET %2$s FROM %3$I.%1$I s WHERE %4$s AND (%5$s)");
45
- expect(sql).toContain("INSERT INTO senat.%1$I (%2$s) SELECT %3$s FROM %4$I.%1$I s WHERE NOT EXISTS (SELECT 1 FROM senat.%1$I t WHERE %5$s)");
46
+ expect(sql).toContain("INSERT INTO senat.%1$I (%2$s) '");
47
+ expect(sql).toContain("SELECT %3$s FROM %4$I.%1$I s '");
48
+ expect(sql).toContain("(SELECT 1 FROM senat.%1$I t WHERE %5$s)");
46
49
  expect(sql).toContain("ALTER TABLE senat.%I ADD CONSTRAINT %I %s");
47
50
  expect(sql).toContain("DROP SEQUENCE IF EXISTS senat.%I CASCADE");
48
51
  expect(sql).not.toContain("DROP SCHEMA IF EXISTS dosleg_staging CASCADE;");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/senat",
3
- "version": "3.1.21",
3
+ "version": "3.1.23",
4
4
  "description": "Handle French Sénat's open data",
5
5
  "keywords": [
6
6
  "France",
@@ -75,43 +75,45 @@
75
75
  "test:prefixed-tables": "vitest tests/prefixedTables.test.ts tests/validatePrefixedTables.test.ts"
76
76
  },
77
77
  "dependencies": {
78
- "cheerio": "^1.1.2",
79
- "command-line-args": "^6.0.1",
80
- "dotenv": "^17.3.1",
81
- "fast-xml-parser": "^5.3.3",
82
- "fs-extra": "^11.3.3",
83
- "jsdom": "^27.2.0",
78
+ "cheerio": "^1.2.0",
79
+ "command-line-args": "^6.0.2",
80
+ "domhandler": "^6.0.1",
81
+ "dotenv": "^17.4.2",
82
+ "fast-xml-parser": "^5.9.3",
83
+ "fs-extra": "^11.3.6",
84
+ "jsdom": "^29.1.1",
84
85
  "luxon": "^3.7.2",
85
- "node-stream-zip": "^1.8.2",
86
- "p-limit": "^7.2.0",
87
- "pg-cursor": "^2.19.0",
88
- "postgres": "^3.4.8",
89
- "slug": "^11.0.0",
86
+ "node-stream-zip": "^1.15.0",
87
+ "p-limit": "^7.3.0",
88
+ "pg": "^8.22.0",
89
+ "pg-cursor": "^2.21.0",
90
+ "slug": "^11.0.1",
90
91
  "windows-1252": "^3.0.4",
91
- "zod": "^4.3.5"
92
+ "zod": "^4.4.3"
92
93
  },
93
94
  "devDependencies": {
94
- "@types/cheerio": "^1.0.0",
95
- "@types/command-line-args": "^5.0.0",
95
+ "@eslint/js": "^10.0.1",
96
+ "@types/command-line-args": "^5.2.3",
96
97
  "@types/fs-extra": "^11.0.4",
97
- "@types/jsdom": "^27.0.0",
98
- "@types/luxon": "^3.7.1",
99
- "@types/node": "^24.10.1",
98
+ "@types/jsdom": "^28.0.3",
99
+ "@types/luxon": "^3.7.2",
100
+ "@types/node": "^26.1.1",
100
101
  "@types/pg": "^8.20.0",
101
102
  "@types/pg-cursor": "^2.7.2",
102
103
  "@types/slug": "^5.0.9",
103
- "@typescript-eslint/eslint-plugin": "^8.52.0",
104
- "@typescript-eslint/parser": "^8.52.0",
104
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
105
+ "@typescript-eslint/parser": "^8.63.0",
105
106
  "cross-env": "^10.1.0",
106
- "eslint": "^9.39.2",
107
- "globals": "^17.0.0",
108
- "kanel": "^4.0.1",
107
+ "eslint": "^10.6.0",
108
+ "extract-pg-schema": "^5.8.2",
109
+ "globals": "^17.7.0",
110
+ "kanel": "^4.0.2",
109
111
  "kanel-kysely": "^4.0.0",
110
112
  "kanel-zod": "^4.0.0",
111
- "kysely": "^0.28.16",
112
- "prettier": "^3.5.3",
113
- "tsx": "^4.21.0",
114
- "typescript": "^5.9.3",
115
- "vitest": "^4.0.18"
113
+ "kysely": "^0.29.3",
114
+ "prettier": "^3.9.4",
115
+ "tsx": "^4.23.0",
116
+ "typescript": "^6.0.3",
117
+ "vitest": "^4.1.10"
116
118
  }
117
119
  }