Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/lod-wiki/senato/sedute-commissione.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ FILTER( ?date >= "2026-05-01"^^xsd:date && ?date <= "2026-05-31"^^xsd:date )
| **Doppia etichetta della commissione** | Una `Commissione` può avere **più `osr:titoloBreve`** (es. `commissione/0-2` → "Giustizia" **e** "Giustizia e autorizzazioni a procedere"). Non mettere `?comm`/`?tb` nel `SELECT` senza aggregazione, altrimenti ogni seduta si duplica. Usare `MIN(?tb)` + `GROUP BY` senza `?comm`. |
| **`SAMPLE` non supportato** | Virtuoso Senato rifiuta `SAMPLE` (400). `MIN`/`MAX`/`COUNT` funzionano. |
| **Niente `BIND`** | Come da [[trappole]] generali del Senato. |
| **`osr:commissione` può puntare a URI mai descritti** | Le sedute referenziano organi `commissione/2-*` che **non esistono come risorse**: zero triple come soggetto, nessuna `osr:Commissione` con quel prefisso (0 su 279). Non hanno titolo né categoria, e il nome non è ricavabile da nessuna parte del grafo — nemmeno dalle sedute, che espongono solo data/tipo/legislatura. Nella sola leg. 19 sono **16 organi**, incluso `commissione/2-1` con **647 sedute**, il secondo più attivo. Conseguenza pratica: se `osr:titoloBreve` è un pattern obbligatorio nella query, questi organi **spariscono in silenzio** e si perde metà dell'attività di commissione. Tenerlo `OPTIONAL` e accettare righe senza nome. (Verificato 2026-07-26.) |
| **Filtrare per legislatura via sedute esclude le Giunte** | Gli organi Senato non hanno una proprietà "legislatura": il filtro naturale passa da `?seduta osr:legislatura N`. Ma le **Giunte non hanno alcuna `SedutaCommissione`** (vedi *Assenti*), quindi con quel criterio scompaiono pur essendo attive. Alternativa: `osr:denominazione` è un nodo con `osr:inizio`/`osr:fine` (stesso pattern temporale dei gruppi), e l'intervallo della legislatura si ricava dal grafo stesso con `MIN`/`MAX` di `osr:dataSeduta` sulle sedute di quella legislatura. I due criteri vanno **uniti**: nessuno dei due basta. (Verificato 2026-07-26.) |

# Ricerca commissione per nome

Expand Down Expand Up @@ -183,6 +185,8 @@ Un tool "audizioni Senato" è **fattibile via scraping HTML** (non SPARQL, non J

# Assenti

* **Sedute delle Giunte**: nessuna delle 4 Giunte del Senato (`commissione/0-20` Regolamento, `0-21` Elezioni e immunità parlamentari, `0-22` Biblioteca e archivio storico, `0-24` Verifica dei poteri) ha `osr:SedutaCommissione` collegate, in nessuna legislatura. L'anagrafica c'è ed è completa (`0-21`: 11 triple, `categoriaCommissione` = `Giunte`, sottotitolo "(Art. 19 del Regolamento)", tre `osr:denominazione` datate dal 1948), ma dell'attività non c'è traccia. È un'assenza rilevante: la Giunta delle elezioni e delle immunità è la sede delle autorizzazioni a procedere, cioè esattamente ciò che si cerca quando una vicenda giudiziaria tocca un parlamentare. (Verificato 2026-07-26.)

* **Commissioni bicamerali con sedute/interventi esposti**: la *Commissione parlamentare di inchiesta sul femminicidio* esiste come entità Senato (`commissione/0-141` storica leg. XVII–XVIII e `commissione/4-223` attuale XIX) con i suoi membri senatori (**24 afferenze** nella XIX), ma **non ha `SedutaCommissione`/`Intervento` collegati** nel LOD Senato (verificato 2026-07-01: 0 righe su entrambi gli URI per leg. 19). Le sedute/interventi sono pubblicati solo dalla Camera (`ocd:organo` `o19_3941`, 181 URI seduta / 157 date distinte fino a giu 2026). Vedi [[../camera/sedute-commissione]]. La composizione Senatori si ottiene con `committee-members list --committee-uri <commissione/4-223> --chamber senato`.

# Citations
Expand All @@ -192,3 +196,5 @@ Un tool "audizioni Senato" è **fattibile via scraping HTML** (non SPARQL, non J
[3] Verifica assenza attività femminicidio Senato (2026-07-01): `committee-sessions`/SPARQL su `commissione/0-141` e `4-223` → 0 sedute, 0 interventi.
[4] Risposta Webmaster Senato (email 2026-07-06): conferma che lo SPARQL non espone resoconto/OdG per `SedutaCommissione`; indica le liste JSON `listasommcomm` come fonte con link ai resoconti.
[5] Schema JSON verificato via agent-browser (2026-07-06): `https://www.senato.it/static/bgt/listasommcomm/0/1/t/19/2026/index.json`; `curl` → HTTP 202 vuoto (WAF). Pattern link resoconto ricavato dai `href` della pagina resoconti: `show-doc?leg=19&tipodoc=SommComm&id=<id_testo>&idoggetto=0`.
[6] URI di commissione orfani (2026-07-26): `SELECT (COUNT(*) AS ?n) WHERE { <http://dati.senato.it/commissione/2-1> ?p ?o }` → `0`; `SELECT (COUNT(DISTINCT ?c) AS ?n) WHERE { ?c a osr:Commissione . FILTER(STRSTARTS(STR(?c), "http://dati.senato.it/commissione/2-")) }` → `0`, contro 279 `osr:Commissione` totali. Organi con sedute in leg. 19: 27, di cui 16 senza `osr:titoloBreve` (tutti `2-*`).
[7] Assenza sedute delle Giunte (2026-07-26): enumerazione di tutte le commissioni con `SedutaCommissione` in leg. 19 → nessuna delle 4 Giunte. Intervallo leg. 19 da `MIN`/`MAX` di `osr:dataSeduta`: `2022-10-25` → `2026-07-23`; commissioni con `osr:denominazione` valida in quel periodo: 59.
67 changes: 67 additions & 0 deletions src/tools/committees.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect } from "vitest";
import { mergeSenatoCommittees } from "./committees.js";

const row = (o: Partial<Record<string, string>> & { uri: string }) => ({
chamber: "senato",
uri: o.uri,
title: o.title ?? "",
short_title: o.short_title ?? "",
subtitle: o.subtitle ?? "",
category: o.category ?? "",
session_count: o.session_count ?? "",
});

// I due insiemi si intersecano solo in parte (#82): le Giunte hanno anagrafica
// ma non sedute, gli organi "2-*" hanno sedute ma non esistono come risorse.
describe("mergeSenatoCommittees", () => {
it("tiene gli organi con sedute e ne completa l'anagrafica", () => {
const out = mergeSenatoCommittees(
[row({ uri: "c/0-1", short_title: "Affari Costituzionali", session_count: "742" })],
[row({ uri: "c/0-1", title: "1a Commissione", short_title: "Affari Costituzionali", category: "Commissioni permanenti" })],
);
expect(out).toHaveLength(1);
expect(out[0].session_count).toBe("742");
expect(out[0].title).toBe("1a Commissione");
expect(out[0].category).toBe("Commissioni permanenti");
});

it("aggiunge gli organi descritti ma senza sedute, con conteggio 0 (le Giunte)", () => {
const out = mergeSenatoCommittees(
[row({ uri: "c/0-1", short_title: "Affari Costituzionali", session_count: "742" })],
[row({ uri: "c/0-21", short_title: "Giunta delle elezioni", category: "Giunte" })],
);
expect(out.map((r) => r.uri)).toEqual(["c/0-1", "c/0-21"]);
expect(out[1].session_count).toBe("0");
});

it("tiene gli organi con sedute ma privi di anagrafica, senza inventarne il nome", () => {
const out = mergeSenatoCommittees([row({ uri: "c/2-1", session_count: "647" })], []);
expect(out).toHaveLength(1);
expect(out[0].short_title).toBe("");
expect(out[0].title).toBe("");
expect(out[0].session_count).toBe("647");
});

it("ordina per numero di sedute decrescente, poi per titolo", () => {
const out = mergeSenatoCommittees(
[
row({ uri: "c/0-5", short_title: "Bilancio", session_count: "592" }),
row({ uri: "c/2-1", session_count: "647" }),
],
[
row({ uri: "c/0-22", short_title: "Biblioteca", category: "Giunte" }),
row({ uri: "c/0-21", short_title: "Giunta delle elezioni", category: "Giunte" }),
],
);
expect(out.map((r) => r.uri)).toEqual(["c/2-1", "c/0-5", "c/0-22", "c/0-21"]);
});

it("non duplica un organo presente in entrambi gli insiemi", () => {
const out = mergeSenatoCommittees(
[row({ uri: "c/0-1", session_count: "742" })],
[row({ uri: "c/0-1", short_title: "Affari Costituzionali" })],
);
expect(out).toHaveLength(1);
expect(out[0].short_title).toBe("Affari Costituzionali");
});
});
120 changes: 104 additions & 16 deletions src/tools/committees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const inputSchema = z.object({
.positive()
.optional()
.describe(
"Numero legislatura. Camera: le commissioni sono istanze per-legislatura, default 19. Senato: opzionale, senza filtro mostra tutte le commissioni storiche (nessun session_count); con filtro mostra solo quelle attive con numero di sedute.",
"Numero legislatura. Camera: le commissioni sono istanze per-legislatura, default 19. Senato: opzionale, senza filtro mostra tutte le commissioni storiche (nessun session_count); con filtro mostra gli organi attivi in quella legislatura, con il numero di sedute (0 per quelli che nel LOD non ne hanno, come le Giunte).",
),
limit: z.number().int().min(1).max(1000).default(300),
});
Expand Down Expand Up @@ -73,25 +73,122 @@ LIMIT ${limit}`;

// --- Senato ---------------------------------------------------------------

// Mappatura comune alle query Senato: i nomi di variabile coincidono, cambia
// solo quali sono valorizzate (le sedute danno `n_sedute`, l'anagrafica i
// titoli). I campi assenti restano stringa vuota.
function toRows(raw: Record<string, string | undefined>[]): Row[] {
return raw.map((r) => ({
chamber: "senato",
uri: r.comm ?? "",
title: r.titolo ?? "",
short_title: r.titoloBreve ?? "",
subtitle: r.sottotitolo ?? "",
category: r.cat ?? r.categoria ?? "",
session_count: r.n_sedute ?? "",
}));
}

// Intervallo di date coperto da una legislatura, ricavato dalle sedute di
// commissione: il grafo Senato non espone inizio/fine legislatura, ma le date
// delle sedute la delimitano con precisione sufficiente a filtrare gli organi.
async function senatoLegislatureRange(
legislature: number,
): Promise<{ from: string; to: string } | null> {
const query = `${OSR_PREFIXES}
SELECT (MIN(?d) AS ?dal) (MAX(?d) AS ?al)
WHERE {
?s a osr:SedutaCommissione .
?s osr:legislatura ${legislature} .
?s osr:dataSeduta ?d
}`;
const raw = flattenBindings(await snQuery(query));
const from = raw[0]?.dal;
const to = raw[0]?.al;
return from && to ? { from, to } : null;
}

/**
* Unisce gli organi con sedute nella legislatura e quelli descritti come
* attivi nel suo periodo. Sono due insiemi che si intersecano solo in parte,
* e nessuno dei due basta da solo (#82): le Giunte non hanno sedute nel LOD,
* e 16 organi con sedute — incluso il secondo più attivo della leg. 19 — non
* esistono come risorse, quindi non hanno né titolo né categoria.
* I campi vuoti restano vuoti: il nome mancante è un buco della fonte, non
* qualcosa da riempire a valle.
*/
export function mergeSenatoCommittees(withSessions: Row[], described: Row[]): Row[] {
const byUri = new Map<string, Row>();
for (const r of withSessions) byUri.set(r.uri, { ...r });
for (const d of described) {
const seen = byUri.get(d.uri);
if (!seen) {
byUri.set(d.uri, { ...d, session_count: "0" });
continue;
}
// L'anagrafica della risorsa è più ricca del solo titoloBreve aggregato
// sulle sedute: completa i campi mancanti senza toccare il conteggio.
seen.title ||= d.title;
seen.short_title ||= d.short_title;
seen.subtitle ||= d.subtitle;
seen.category ||= d.category;
}
return [...byUri.values()].sort(
(a, b) =>
Number(b.session_count || 0) - Number(a.session_count || 0) ||
a.short_title.localeCompare(b.short_title),
);
}

async function querySenato(
legislature: number | undefined,
limit: number,
): Promise<Row[]> {
let query: string;
if (legislature) {
// Filtrate per legislatura via SedutaCommissione, con conteggio sedute.
// Le due query lavorano su insiemi piccoli e noti (nella leg. 19: 27 organi
// con sedute, 59 descritti attivi), quindi il `limit` dell'utente si applica
// DOPO il merge: troncare prima toglierebbe righe che l'ordinamento finale
// avrebbe messo in testa. Il tetto interno esiste solo come guardia.
const cap = 1000;
// Organi con sedute nella legislatura. `titoloBreve` è OPTIONAL: quando
// manca l'organo va mostrato lo stesso, altrimenti sparisce in silenzio
// proprio chi ha più sedute (#82).
query = `${OSR_PREFIXES}
SELECT ?comm (MIN(?titoloBreve) AS ?titoloBreve) (MIN(?cat) AS ?categoria) (COUNT(DISTINCT ?seduta) AS ?n_sedute)
WHERE {
?seduta a osr:SedutaCommissione .
?seduta osr:commissione ?comm .
?seduta osr:legislatura ${legislature} .
?comm osr:titoloBreve ?titoloBreve .
OPTIONAL { ?comm osr:titoloBreve ?titoloBreve }
OPTIONAL { ?comm osr:categoriaCommissione ?cat }
}
GROUP BY ?comm
ORDER BY DESC(?n_sedute)
LIMIT ${limit}`;
LIMIT ${cap}`;
const withSessions = toRows(flattenBindings(await snQuery(query)));
const range = await senatoLegislatureRange(legislature);
if (!range) return withSessions;
// Organi la cui denominazione è valida nel periodo della legislatura:
// stesso pattern temporale dei gruppi (osr:denominazione con inizio/fine),
// ed è l'unico modo per vedere le Giunte, che sedute nel LOD non ne hanno.
const activeQuery = `${OSR_PREFIXES}
SELECT DISTINCT ?comm ?titolo ?titoloBreve ?sottotitolo ?cat
WHERE {
?comm a osr:Commissione .
?comm osr:denominazione ?den .
?den osr:inizio ?ini .
OPTIONAL { ?den osr:fine ?fine }
OPTIONAL { ?comm osr:titolo ?titolo }
OPTIONAL { ?comm osr:titoloBreve ?titoloBreve }
OPTIONAL { ?comm osr:sottotitolo ?sottotitolo }
OPTIONAL { ?comm osr:categoriaCommissione ?cat }
FILTER(str(?ini) <= "${range.to}")
FILTER(!BOUND(?fine) || str(?fine) >= "${range.from}")
}
ORDER BY ?comm
LIMIT ${cap}`;
const described = toRows(flattenBindings(await snQuery(activeQuery)));
return mergeSenatoCommittees(withSessions, described).slice(0, limit);
} else {
// Tutte le commissioni storiche, nessun conteggio sedute.
query = `${OSR_PREFIXES}
Expand All @@ -106,25 +203,16 @@ WHERE {
ORDER BY ?titoloBreve
LIMIT ${limit}`;
}
const results = await snQuery(query);
const raw = flattenBindings(results);
return raw.map((r) => ({
chamber: "senato",
uri: r.comm ?? "",
title: r.titolo ?? "",
short_title: r.titoloBreve ?? "",
subtitle: r.sottotitolo ?? "",
category: r.cat ?? r.categoria ?? "",
session_count: r.n_sedute ?? "",
}));
return toRows(flattenBindings(await snQuery(query)));
}

export const committeesTool: Tool<typeof inputSchema> = {
name: "committees",
description:
"[CAMERA+SENATO] Commissioni parlamentari (permanenti, speciali, d'inchiesta monocamerali e bicamerali, giunte, comitati). " +
"Camera: filtrata per legislatura (default 19; le commissioni sono istanze per-legislatura), con dc:type come categoria. " +
"Senato: senza --legislature mostra tutte le commissioni storiche (nessun session_count); con --legislature filtra le attive e aggiunge il numero di sedute.",
"Senato: senza --legislature mostra tutte le commissioni storiche (nessun session_count); con --legislature unisce due insiemi — gli organi con sedute in quella legislatura (con il conteggio) e quelli la cui denominazione risulta valida nel periodo (session_count 0). Serve entrambi perché le Giunte non hanno sedute nel LOD e comparirebbero solo nel secondo. " +
"ATTENZIONE (limite della fonte, non del tool): alcuni organi del Senato con molte sedute — nella leg. 19 sedici, incluso il secondo più attivo — sono referenziati con URI 'commissione/2-*' che nel grafo non esistono come risorse: niente titolo, niente categoria. Le loro righe hanno i campi di testo VUOTI e solo uri + session_count. Non dedurre il nome dell'organo dall'URI o dal numero di sedute: non è ricavabile dal dato.",
inputSchema,
examples: [
"italianparliament committees list --chamber camera --legislature 19",
Expand Down
37 changes: 37 additions & 0 deletions src/tools/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,43 @@ describe("Senato tools", () => {
expect(Number(affari!.session_count)).toBeGreaterThan(100);
}, 30000);

it("committees (Senato): le Giunte compaiono anche se nel LOD non hanno sedute (#82)", async () => {
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 300 });
const giunte = result.rows.filter((r) => r.category === "Giunte");
expect(giunte.length).toBeGreaterThanOrEqual(2);
// La Giunta elezioni e immunità è l'organo delle autorizzazioni a procedere:
// prima del fix era invisibile con --legislature, perché il filtro passava
// dalle sedute e per le Giunte il LOD non ne espone nessuna.
const immunita = giunte.find((r) => r.short_title.includes("Giunta delle elezioni"));
expect(immunita).toBeDefined();
expect(immunita!.session_count).toBe("0");
}, 30000);

it("committees (Senato): gli organi privi di anagrafica nella fonte restano visibili, senza nome inventato (#82)", async () => {
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 300 });
// URI referenziati dalle sedute ma inesistenti come risorse (commissione/2-*):
// devono comparire con le sedute e i campi di testo vuoti.
const orfani = result.rows.filter((r) => r.short_title === "" && Number(r.session_count) > 0);
expect(orfani.length).toBeGreaterThan(0);
expect(Math.max(...orfani.map((r) => Number(r.session_count)))).toBeGreaterThan(100);
for (const r of orfani) {
expect(r.title).toBe("");
expect(r.category).toBe("");
expect(r.uri).not.toBe("");
}
}, 30000);

it("committees (Senato): --limit tronca dopo il merge, non dentro le query (#82)", async () => {
// Il limite deve tagliare la classifica finale — i più attivi in testa — e
// non le due query intermedie, dove taglierebbe righe che l'ordinamento
// avrebbe promosso.
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 5 });
expect(result.rows.length).toBe(5);
const counts = result.rows.map((r) => Number(r.session_count));
expect(counts).toEqual([...counts].sort((a, b) => b - a));
expect(counts[0]).toBeGreaterThan(500);
}, 30000);

it("committees: returns Camera committees for leg 19", async () => {
const result = await committeesTool.execute({ chamber: "camera", legislature: 19, limit: 300 });
expect(result.rows.length).toBeGreaterThan(10);
Expand Down
Loading