@tricoteuses/senat
Version:
Handle French Sénat's open data
155 lines (154 loc) • 5.8 kB
JavaScript
import { getSessionsFromStart } from "../types/sessions";
import { iterLoadSenatDossiersLegislatifs } from "../loaders";
export function buildOdj(events, dossierBySenatUrl) {
const byObjet = new Map(); // objet -> set de dossier uids
let codeEtape = null;
let dossier = null;
for (const ev of events) {
const objetKey = (ev.objet ?? "").trim();
const url = normalizeSenatUrl(ev.urlDossierSenat) ?? undefined;
dossier = url ? dossierBySenatUrl[url] : null;
const dossierUid = dossier ? pickDossierUid(dossier) : undefined;
codeEtape = dossier ? computeCodeEtape(ev, dossier) : null;
// si on n’a ni objet ni dossier, ça ne sert à rien de créer un point
if (!objetKey && !dossierUid)
continue;
if (!byObjet.has(objetKey) && dossierUid) {
byObjet.set(objetKey, dossierUid);
}
}
if (byObjet.size === 0)
return undefined;
const pointsOdj = [];
for (const [objetKey, dossierUid] of byObjet) {
pointsOdj.push({
objet: objetKey || null,
dossierLegislatifRef: dossierUid || null,
codeEtape,
});
}
return { pointsOdj };
}
function pickDossierUid(d) {
if (d["signet"] && d["signet"].trim())
return d["signet"].trim();
if (d["code"] && String(d["code"]).trim())
return String(d["code"]).trim();
return undefined;
}
function normalizeSenatUrl(url) {
if (!url)
return null;
let u = url.trim();
if (!u)
return null;
if (!/^https?:\/\//i.test(u))
return u;
// force https://
u = u.replace(/^http:\/\//i, "https://");
u = u.replace(/\/+$/, "");
return u;
}
export function buildSenatDossierIndex(options) {
const index = {};
const sessions = getSessionsFromStart(2015);
for (const session of sessions) {
for (const item of iterLoadSenatDossiersLegislatifs(options["dataDir"], session)) {
const dossier = item.item;
const url = dossier["url"] ? normalizeSenatUrl(dossier["url"]) : undefined;
if (url)
index[url] = dossier;
}
}
return index;
}
function detectLecture(objet) {
objet = objet.toLowerCase();
if (objet.includes("première lecture"))
return 1;
if (objet.includes("deuxième lecture") || objet.includes("2ème"))
return 2;
if (objet.includes("troisième lecture") || objet.includes("3ème"))
return 3;
return undefined;
}
function computeCodeEtape(ev, dossier) {
// In order to match with stage, we need to remove the '-SEANCE' suffix from the codeActe
const cleanCode = (code) => code.replace(/-SEANCE$/, "");
const lecture = detectLecture(ev.objet ?? "");
const organe = ev.organe ?? "";
const nature = organe.toLowerCase().includes("commission")
? "COM"
: organe.toLowerCase().includes("séance publique")
? "DEBATS"
: "";
const evDate = ev.date.split("T")[0];
const flat = buildFlatActes(dossier);
// 1) Strict matching: same date + same nature
let candidates = flat.filter((a) => {
if (a.date !== evDate)
return false;
if (nature && !a.codeActe.includes(nature))
return false;
return true;
});
// If a specific lecture is detected in the agenda event, refine the candidates
if (lecture !== undefined && candidates.length > 0) {
const withLecture = candidates.filter((c) => c.ordreLecture === lecture);
if (withLecture.length > 0) {
candidates = withLecture;
}
}
if (candidates.length > 0) {
// Multiple candidates: pick the most specific one (longest code string)
candidates.sort((a, b) => b.codeActe.length - a.codeActe.length);
return cleanCode(candidates[0].codeActe);
}
// 2) Fallback COM: If no exact date match for a commission event,
// take the latest commission act for this lecture on or before the event date.
if (nature === "COM") {
let comActs = flat.filter((a) => a.codeActe.includes("COM") && a.date <= evDate);
if (lecture !== undefined) {
const byLecture = comActs.filter((a) => a.ordreLecture === lecture);
if (byLecture.length > 0)
comActs = byLecture;
}
if (comActs.length > 0) {
comActs.sort((a, b) => b.date.localeCompare(a.date) || b.codeActe.length - a.codeActe.length);
return cleanCode(comActs[0].codeActe);
}
}
// 3) Fallback general lecture: if nothing else worked but a lecture is identified,
// find any act belonging to that lecture (e.g., SN1-DEPOT).
if (lecture !== undefined) {
const genericActe = flat.find((a) => a.ordreLecture === lecture);
if (genericActe) {
return cleanCode(genericActe.codeActe);
}
}
console.log(`✖ No stage code found for ev=${ev.id} (Date: ${evDate}, Nature: ${nature}, Lecture: ${lecture})`, {
totalActsInDossier: dossier["actes_legislatifs"]?.length || 0,
firstActDate: flat[0]?.date,
});
return null;
}
function buildFlatActes(dossier) {
const actes = dossier["actes_legislatifs"] ?? [];
const res = [];
for (const acte of actes) {
if (acte["chambre"] !== "SN")
continue;
const codeActe = acte.code_acte;
const dateActe = acte.date?.split("T")[0];
if (!codeActe || !dateActe)
continue;
const match = codeActe.match(/^(?:SN|AN)(\d+)/);
const ordreLecture = match ? parseInt(match[1], 10) : undefined;
res.push({
codeActe,
date: dateActe,
ordreLecture,
});
}
return res;
}